From dcaf07c68b62923491ddaaaad2b70865ac174a8e Mon Sep 17 00:00:00 2001 From: QwejayHuang Date: Wed, 26 Aug 2026 09:53:30 +0800 Subject: [PATCH] v2.7.13: Optimize Nuitka Lite Mode, enhance security, and fix backport crash - Added `--lto=yes` Link-Time Optimization for Nuitka in Lite Mode - Fixed package bloat in Lite Mode - Fixed fatal TypeError in Backport exclusion rule version comparison - Cleaned up bare except clauses to avoid swallowing interrupt signals - Upgraded hashing algorithm to SHA-256 and added download protocol validation - Passed Ruff, Flake8, and Bandit security and quality audits - Added `__future__.annotations` for Python 3.8/3.9 type hint compatibility - Cleaned up redundant AST dependency logs when requirements.txt is absent - Fixed missing dynamic translation for the "Code Signing" card title --- CHANGELOG.md | 18 + CHANGELOG_EN.md | 18 + pyproject.toml | 2 +- qpypack/main.py | 4311 +++++++++++++++++++++++++++++------------------ 4 files changed, 2704 insertions(+), 1645 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 374b753..513d151 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,24 @@ QPyPack 项目的所有重要变更都将记录在此文件中。 --- +### [2.7.13] - 2026-08-26 + +#### 新增 +- **Nuitka 编译优化**:精简模式下默认引入 `--lto=yes` 链接时间优化机制,深度消除未引用符号,大幅压缩编译出的原生二进制体积。 + +#### 改进 +- **代码安全与规范全面升级**:全量通过 Ruff、Flake8 及 Bandit 安全审计,哈希算法升级为更安全的 SHA-256,并增加 Python 下载协议的白名单与异常链校验。 +- **日志显示改进**:优化 AST 静态分析与依赖清单的比对逻辑,未提供 `requirements.txt` 时不再冗余输出重复的“自动补齐”项,日志层级更清晰明了。 +- **低版本解释器类型兼容**:引入 `__future__.annotations` 延迟注解机制,确保现代 PEP 604 类型提示在 Python 3.8 / 3.9 环境下依然稳定运行不崩溃。 + +#### 修复 +- **修复精简模式BUG**:修复精简模式下打包体积暴增的问题。 +- **修复版本比对致命异常**:重构 Backport 拦截规则的版本解析逻辑,修复目标解释器元组与规则字符串直接比对导致的崩溃问题。 +- **修复界面多语言刷新遗漏**:修复多语言即时切换时“代码签名”卡片标题未能同步翻译的问题,并清理了翻译字典中的重复键声明。 +- **消除进程退出拦截隐患**:清理所有裸 `except:`,避免意外拦截系统中断信号导致子线程无法正常退出。 + +--- + ## [2.7.12] - 2026-08-24 ### 新增 diff --git a/CHANGELOG_EN.md b/CHANGELOG_EN.md index 599e759..39b8dc3 100644 --- a/CHANGELOG_EN.md +++ b/CHANGELOG_EN.md @@ -9,6 +9,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +### [2.7.13] - 2026-08-26 + +#### Added +- **Nuitka Compilation Optimization**: Enabled `--lto=yes` (Link-Time Optimization) by default in Lite Mode to eliminate unreferenced symbols and significantly reduce the compiled native binary size. + +#### Improved +- **Code Security & Quality Upgrades**: Fully passed Ruff, Flake8, and Bandit security audits; upgraded internal hashing to SHA-256; added protocol whitelisting and explicit exception chaining for Python downloads. +- **Log Output Refinements**: Optimized comparison logic between AST analysis and dependency manifests; eliminated redundant "Auto-patched" logs when no `requirements.txt` is present for cleaner, more intuitive logs. +- **Legacy Python Compatibility**: Introduced `from __future__ import annotations` deferred evaluation, ensuring modern PEP 604 union type hints run reliably on Python 3.8 / 3.9 without runtime errors. + +#### Fixed +- **Fix Lite Mode Bloat Issue**: Resolved an issue causing bloated package sizes in Lite Mode. +- **Fix Fatal Version Comparison Crash**: Refactored version parsing in Backport exclusion rules, resolving a crash caused by comparing version tuples directly with rule strings (`TypeError`). +- **Fix Missing UI Translation Refresh**: Fixed a dynamic translation refresh issue where the "Code Signing" card title failed to update on language switch, and removed duplicate translation keys. +- **Eliminate Process Interception Risk**: Replaced all bare `except:` clauses to prevent swallowing system interrupt signals, ensuring worker threads terminate cleanly. + +--- + ## [2.7.12] - 2026-08-25 ### Added diff --git a/pyproject.toml b/pyproject.toml index e7dfa0d..1b86d38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "qpypack" -version = "2.7.12" +version = "2.7.13" description = "A cross-platform GUI packaging and compilation suite based on PyInstaller and Nuitka." readme = "README_EN.md" authors = [ diff --git a/qpypack/main.py b/qpypack/main.py index f4f9fd1..39583c5 100644 --- a/qpypack/main.py +++ b/qpypack/main.py @@ -1,35 +1,41 @@ -import sys -import os -import shutil -import subprocess -import tempfile -import re -import time -import stat -import json -import math -import wave -import struct -import traceback -import threading -import configparser -import locale +from __future__ import annotations + import ast +import configparser +import json +import locale +import logging +import math +import os +import re +import shutil +import stat +import struct +import subprocess +import sys +import tempfile +import threading +import time +import traceback +import wave from concurrent.futures import ThreadPoolExecutor from pathlib import Path -import logging _orig_resolve = Path.resolve + + def _safe_resolve(self, strict=False): try: return _orig_resolve(self, strict=strict) except Exception: return self.absolute() + + Path.resolve = _safe_resolve logger = logging.getLogger(__name__) -if os.name == 'nt': +if os.name == "nt": try: import winreg except ImportError: @@ -39,29 +45,87 @@ else: os.environ["QT_LOGGING_RULES"] = "qt.text.font.db=false;qt.multimedia*=false" -from PySide6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QWidget, - QPushButton, QLabel, QLineEdit, QFileDialog, QCheckBox, - QComboBox, QFrame, QStackedLayout, QFormLayout, QTextEdit, - QGraphicsOpacityEffect, QGridLayout, QTabWidget, - QMessageBox, QInputDialog, QFileIconProvider, QSizePolicy, QScrollArea, - QGraphicsDropShadowEffect, QSpinBox, QListWidget, QListWidgetItem, - QListView, QStyledItemDelegate, QMenu, QTableWidget, QTableWidgetItem, - QHeaderView, QDialog, QRadioButton) -from PySide6.QtCore import (Qt, QThread, Signal, QPropertyAnimation, QEasingCurve, - QParallelAnimationGroup, QFileInfo, QVariantAnimation, - QTimer, QPointF, QRectF, QRect, QSize, QUrl, QLocale, QObject, QTranslator, QLibraryInfo) -from PySide6.QtGui import (QFont, QDragEnterEvent, QDropEvent, QIcon, QPixmap, - QPainter, QColor, QPen, QImage, QImageWriter, QDesktopServices, QPalette) + +from PySide6.QtCore import ( + QEasingCurve, + QFileInfo, + QLibraryInfo, + QLocale, + QObject, + QParallelAnimationGroup, + QPointF, + QPropertyAnimation, + QRectF, + QSize, + Qt, + QThread, + QTimer, + QTranslator, + QUrl, + QVariantAnimation, + Signal, +) +from PySide6.QtGui import ( + QColor, + QDesktopServices, + QDragEnterEvent, + QDropEvent, + QFont, + QIcon, + QImage, + QImageWriter, + QPainter, + QPalette, + QPen, + QPixmap, +) from PySide6.QtSvg import QSvgRenderer +from PySide6.QtWidgets import ( + QApplication, + QCheckBox, + QComboBox, + QDialog, + QFileDialog, + QFileIconProvider, + QFormLayout, + QFrame, + QGraphicsDropShadowEffect, + QGraphicsOpacityEffect, + QGridLayout, + QHBoxLayout, + QHeaderView, + QLabel, + QLineEdit, + QListView, + QListWidget, + QListWidgetItem, + QMainWindow, + QMenu, + QMessageBox, + QPushButton, + QRadioButton, + QScrollArea, + QSizePolicy, + QSpinBox, + QStackedLayout, + QStyledItemDelegate, + QTableWidget, + QTableWidgetItem, + QTabWidget, + QTextEdit, + QVBoxLayout, + QWidget, +) try: from PySide6.QtMultimedia import QSoundEffect + HAS_QT_AUDIO = True except ImportError: HAS_QT_AUDIO = False __app_name__ = "QPyPack" -__version__ = "2.7.12" +__version__ = "2.7.13" __author__ = "QwejayHuang" __company__ = "QwejayHuang" __description__ = "Modern Cross-Platform Python Packaging GUI Powered by PyInstaller & Nuitka" @@ -462,12 +526,6 @@ ZH_CN_DICT = { "Scanning...": "正在扫描...", "[INFO] Auto-detected existing virtual environment '{venv}'. 'Keep Local Venv' is checked.": "[INFO] 自动检测到现有虚拟环境'{venv}'。已勾选'保留本地虚拟环境'。", "[INFO] Using MinGW64 compiler.": "[INFO] 使用MinGW64编译器。", - "Package Map": "包名映射与规则", - "Package Mapping & Remediation": "包名与规则", - "Obsolete Backport Exclusion Rules": "废弃兼容包拦截规则", - "Min Python Ver": "最低内置版本", - "Add Backport Rule": "添加拦截规则", - "Package Map": "包名映射与规则", "Package Mappings & Rules": "包名映射与规则", "Obsolete Backport Exclusion Rules": "废弃兼容包拦截规则", "Min Python Ver": "最低内置版本", @@ -572,7 +630,17 @@ class TranslationEngine(QObject): return f"{parts[0].lower()}_{parts[1].upper()}" elif len(parts) == 1: lang = parts[0].lower() - mapping = {"zh": "zh_CN", "en": "en_US", "ja": "ja_JP", "ko": "ko_KR", "de": "de_DE", "fr": "fr_FR", "es": "es_ES", "ru": "ru_RU", "pt": "pt_BR"} + mapping = { + "zh": "zh_CN", + "en": "en_US", + "ja": "ja_JP", + "ko": "ko_KR", + "de": "de_DE", + "fr": "fr_FR", + "es": "es_ES", + "ru": "ru_RU", + "pt": "pt_BR", + } return mapping.get(lang, self.DEFAULT_LOCALE) return self.DEFAULT_LOCALE @@ -581,17 +649,25 @@ class TranslationEngine(QObject): sys_locale = QLocale.system().name() except Exception: sys_locale = "" - + if sys_locale.startswith("zh"): - if any(k in sys_locale for k in ("TW", "HK", "MO", "Hant")): return "zh_TW" + if any(k in sys_locale for k in ("TW", "HK", "MO", "Hant")): + return "zh_TW" return "zh_CN" - elif sys_locale.startswith("ja"): return "ja_JP" - elif sys_locale.startswith("ko"): return "ko_KR" - elif sys_locale.startswith("de"): return "de_DE" - elif sys_locale.startswith("fr"): return "fr_FR" - elif sys_locale.startswith("es"): return "es_ES" - elif sys_locale.startswith("ru"): return "ru_RU" - elif sys_locale.startswith("pt"): return "pt_BR" + elif sys_locale.startswith("ja"): + return "ja_JP" + elif sys_locale.startswith("ko"): + return "ko_KR" + elif sys_locale.startswith("de"): + return "de_DE" + elif sys_locale.startswith("fr"): + return "fr_FR" + elif sys_locale.startswith("es"): + return "es_ES" + elif sys_locale.startswith("ru"): + return "ru_RU" + elif sys_locale.startswith("pt"): + return "pt_BR" return "en_US" def load_all_locales(self): @@ -599,7 +675,7 @@ class TranslationEngine(QObject): if self.locales_dir.exists(): for p in self.locales_dir.glob("*.json"): try: - data = json.loads(p.read_text(encoding='utf-8')) + data = json.loads(p.read_text(encoding="utf-8")) code = self.normalize_locale(p.stem) self.translations[code] = data except Exception as e: @@ -615,7 +691,7 @@ class TranslationEngine(QObject): langs = {"auto": self.t("System Default ({sys_native})", sys_native=sys_native)} all_codes = set(self.translations.keys()) | {self.DEFAULT_LOCALE, "zh_CN"} - + for code in sorted(all_codes): if code in self.LANG_META: langs[code] = self.LANG_META[code]["native"] @@ -625,7 +701,7 @@ class TranslationEngine(QObject): if not native_name or native_name == code: native_name = qloc.languageToString(qloc.language()) langs[code] = native_name.capitalize() if native_name else code - + return langs def update_qt_translator(self, lang_code: str): @@ -639,12 +715,7 @@ class TranslationEngine(QObject): target_locale = self.normalize_locale(lang_code) short_lang = target_locale.split("_")[0] if "_" in target_locale else target_locale - candidates = [ - f"qtbase_{target_locale}", - f"qtbase_{short_lang}", - f"qt_{target_locale}", - f"qt_{short_lang}" - ] + candidates = [f"qtbase_{target_locale}", f"qtbase_{short_lang}", f"qt_{target_locale}", f"qt_{short_lang}"] for name in candidates: if self.qt_translator.load(name, trans_path): @@ -667,20 +738,25 @@ class TranslationEngine(QObject): elif self.current_lang == "zh_CN" and text in self.fallback_zh_cn: val = self.fallback_zh_cn[text] if kwargs: - try: return val.format(**kwargs) - except Exception: pass + try: + return val.format(**kwargs) + except Exception: + pass return val + _LOCALES_DIR = Path(__file__).parent / "locales" if not _LOCALES_DIR.exists(): _LOCALES_DIR = _CONFIG_DIR / "locales" - + I18N = TranslationEngine(_LOCALES_DIR) I18N.init_locale() + def _(text: str, **kwargs) -> str: return I18N.t(text, **kwargs) + PYPI_MIRRORS_GLOBAL = [ ("PyPI Official (Global Default)", "https://pypi.org/simple"), ("Python TestPyPI", "https://test.pypi.org/simple"), @@ -693,42 +769,77 @@ PYPI_MIRRORS_GLOBAL = [ ] DEFAULT_MAPPINGS = { - 'acoustid': 'pyacoustid', 'cv2': 'opencv-python', 'PIL': 'pillow', 'Pillow': 'pillow', - 'skimage': 'scikit-image', 'vlc': 'python-vlc', 'pyzbar': 'pyzbar', 'OpenGL': 'PyOpenGL', - - 'pyside6_addons': 'PySide6', 'pyside6_essentials': 'PySide6', - 'pyside6-addons': 'PySide6', 'pyside6-essentials': 'PySide6', - 'pyqt5-plugins': 'PyQt5', 'pyqt5-tools': 'PyQt5', 'pyqt5_plugins': 'PyQt5', - - 'fitz': 'pymupdf', 'docx': 'python-docx', 'pptx': 'python-pptx', - 'bs4': 'beautifulsoup4', 'barcode': 'python-barcode', 'pdfplumber': 'pdfplumber', - - 'win32com': 'pywin32', 'win32api': 'pywin32', 'win32con': 'pywin32', - 'win32gui': 'pywin32', 'win32clipboard': 'pywin32', 'win32print': 'pywin32', 'win32file': 'pywin32', - 'win32security': 'pywin32', 'win32process': 'pywin32', 'win32evtlog': 'pywin32', 'win32service': 'pywin32', - 'win32pipe': 'pywin32', 'win32net': 'pywin32', 'win32crypt': 'pywin32', 'pythoncom': 'pywin32', - 'pywintypes': 'pywin32', - - 'serial': 'pyserial', 'usb': 'pyusb', 'bluetooth': 'pybluez', 'dns': 'dnspython', - 'websocket': 'websocket-client', 'paho': 'paho-mqtt', 'socketio': 'python-socketio', - 'engineio': 'python-engineio', 'kafka': 'kafka-python', - - 'sklearn': 'scikit-learn', 'yaml': 'pyyaml', 'dateutil': 'python-dateutil', 'jwt': 'PyJWT', - 'Crypto': 'pycryptodome', 'wx': 'wxPython', 'desktop_notifier': 'desktop-notifier', - 'dotenv': 'python-dotenv', 'telegram': 'python-telegram-bot', 'git': 'GitPython', - 'github': 'PyGithub', 'gitlab': 'python-gitlab', 'discord': 'discord.py', - 'OpenSSL': 'pyOpenSSL', 'ldap': 'python-ldap', 'magic': 'python-magic', 'slugify': 'python-slugify', - 'snappy': 'python-snappy', - - 'attr': 'attrs', 'psycopg2': 'psycopg2-binary', - - 'pkg_resources': 'setuptools', - 'google.protobuf': 'protobuf', - 'dotenv': 'python-dotenv', - 'jwt': 'PyJWT', - 'cx_Oracle': 'cx-Oracle', - 'mysql': 'mysql-connector-python', - 'pydantic_core': 'pydantic-core', + "acoustid": "pyacoustid", + "cv2": "opencv-python", + "PIL": "pillow", + "Pillow": "pillow", + "skimage": "scikit-image", + "vlc": "python-vlc", + "pyzbar": "pyzbar", + "OpenGL": "PyOpenGL", + "pyside6_addons": "PySide6", + "pyside6_essentials": "PySide6", + "pyside6-addons": "PySide6", + "pyside6-essentials": "PySide6", + "pyqt5-plugins": "PyQt5", + "pyqt5-tools": "PyQt5", + "pyqt5_plugins": "PyQt5", + "fitz": "pymupdf", + "docx": "python-docx", + "pptx": "python-pptx", + "bs4": "beautifulsoup4", + "barcode": "python-barcode", + "pdfplumber": "pdfplumber", + "win32com": "pywin32", + "win32api": "pywin32", + "win32con": "pywin32", + "win32gui": "pywin32", + "win32clipboard": "pywin32", + "win32print": "pywin32", + "win32file": "pywin32", + "win32security": "pywin32", + "win32process": "pywin32", + "win32evtlog": "pywin32", + "win32service": "pywin32", + "win32pipe": "pywin32", + "win32net": "pywin32", + "win32crypt": "pywin32", + "pythoncom": "pywin32", + "pywintypes": "pywin32", + "serial": "pyserial", + "usb": "pyusb", + "bluetooth": "pybluez", + "dns": "dnspython", + "websocket": "websocket-client", + "paho": "paho-mqtt", + "socketio": "python-socketio", + "engineio": "python-engineio", + "kafka": "kafka-python", + "sklearn": "scikit-learn", + "yaml": "pyyaml", + "dateutil": "python-dateutil", + "jwt": "PyJWT", + "Crypto": "pycryptodome", + "wx": "wxPython", + "desktop_notifier": "desktop-notifier", + "dotenv": "python-dotenv", + "telegram": "python-telegram-bot", + "git": "GitPython", + "github": "PyGithub", + "gitlab": "python-gitlab", + "discord": "discord.py", + "OpenSSL": "pyOpenSSL", + "ldap": "python-ldap", + "magic": "python-magic", + "slugify": "python-slugify", + "snappy": "python-snappy", + "attr": "attrs", + "psycopg2": "psycopg2-binary", + "pkg_resources": "setuptools", + "google.protobuf": "protobuf", + "cx_Oracle": "cx-Oracle", + "mysql": "mysql-connector-python", + "pydantic_core": "pydantic-core", } DEFAULT_BACKPORT_RULES = { @@ -744,63 +855,79 @@ DEFAULT_BACKPORT_RULES = { "importlib-resources": "3.9", "zoneinfo": "3.9", "tomllib": "3.11", - "exceptiongroup": "3.11" + "exceptiongroup": "3.11", } MATERIAL_ICONS = { - 'settings': 'M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.06-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.73,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.06,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.43-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.49-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z', - 'refresh': 'M17.65,6.35C16.2,4.9,14.21,4,12,4c-4.42,0-7.99,3.58-7.99,8s3.57,8,7.99,8c3.73,0,6.84-2.55,7.73-6h-2.08 c-0.82,2.33-3.04,4-5.65,4c-3.31,0-6-2.69-6-6s2.69-6,6-6c1.66,0,3.14,0.69,4.22,1.78L13,11h7V4L17.65,6.35z', - 'play': 'M8 5v14l11-7z', - 'stop': 'M6 6h12v12H6z', - 'folder': 'M20 6h-8l-2-2H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 12H4V8h16v10z', - 'expand_more': 'M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z', - 'expand_less': 'M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z', - 'check': 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z', - 'package': 'M20,2H4C3,2,2,2.9,2,4v3.01C2,7.73,2.43,8.35,3,8.7V20c0,1.1,1.1,2,2,2h14c0.9,0,2-0.9,2-2V8.7c0.57-0.35,1-0.97,1-1.69V4 C22,2.9,21,2,20,2z M19,20H5V9h14V20z M20,7H4V4h16V7z M9,12h6v2H9V12z', - 'back': 'M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z', - 'info': 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z', - 'python': 'M12.06,1.48c-3.14,0-3.52,0.67-3.52,0.67l-0.01,2.44h3.63v0.52H7.43C5.12,5.11,4.5,6.58,4.5,8.81c0,2.34,0.38,3.48,2.3,3.48 h1.14v-1.62c0-1.48,1.23-2.65,2.7-2.65h3.69c1.47,0,2.66-1.19,2.66-2.65V3.88C16.99,1.83,14.67,1.48,12.06,1.48z M10.22,2.83 c0.41,0,0.73,0.33,0.73,0.74c0,0.41-0.33,0.74-0.73,0.74c-0.4,0-0.73-0.33-0.73-0.74C9.49,3.16,9.82,2.83,10.22,2.83z M16.71,9.89 v1.62c0,1.48-1.23,2.65-2.7,2.65H10.3c-1.47,0-2.66,1.19-2.66,2.65v1.49c0,2.05,2.32,2.41,4.92,2.41c3.14,0,3.52-0.67,3.52-0.67 l0.01-2.44h-3.63v-0.52h4.73c2.31,0,2.93-1.47,2.93-3.7c0-2.34-0.38-3.48-2.3-3.48H16.71z M13.88,18.96c0.41,0,0.73,0.33,0.73,0.74c0,0.41-0.33,0.74-0.73,0.74c-0.4,0-0.73-0.33-0.73-0.74C13.15,19.29,13.48,18.96,13.88,18.96z', - 'close': 'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z', - 'engine': 'M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9-2-2-5-2.4-7.4-1.3L9 6 6 9 1.6 4.6C.4 7 1 10 3 12c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.4-.4.4-1.1 0-1.3z', - 'bolt': 'M11 21h-1l1-7H7.5c-.58 0-.57-.32-.38-.66s.06-.11.08-.15C8.22 11.23 10.3 7.6 13.43 2.15c.18-.32.37-.15.37.15l-1 7h3.5c.58 0 .57.32.38.66s-.06.12-.08.16L11 21z', - 'link': 'M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z' + "settings": "M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.06-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.73,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.06,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.43-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.49-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z", + "refresh": "M17.65,6.35C16.2,4.9,14.21,4,12,4c-4.42,0-7.99,3.58-7.99,8s3.57,8,7.99,8c3.73,0,6.84-2.55,7.73-6h-2.08 c-0.82,2.33-3.04,4-5.65,4c-3.31,0-6-2.69-6-6s2.69-6,6-6c1.66,0,3.14,0.69,4.22,1.78L13,11h7V4L17.65,6.35z", + "play": "M8 5v14l11-7z", + "stop": "M6 6h12v12H6z", + "folder": "M20 6h-8l-2-2H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 12H4V8h16v10z", + "expand_more": "M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z", + "expand_less": "M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z", + "check": "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z", + "package": "M20,2H4C3,2,2,2.9,2,4v3.01C2,7.73,2.43,8.35,3,8.7V20c0,1.1,1.1,2,2,2h14c0.9,0,2-0.9,2-2V8.7c0.57-0.35,1-0.97,1-1.69V4 C22,2.9,21,2,20,2z M19,20H5V9h14V20z M20,7H4V4h16V7z M9,12h6v2H9V12z", + "back": "M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z", + "info": "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z", + "python": "M12.06,1.48c-3.14,0-3.52,0.67-3.52,0.67l-0.01,2.44h3.63v0.52H7.43C5.12,5.11,4.5,6.58,4.5,8.81c0,2.34,0.38,3.48,2.3,3.48 h1.14v-1.62c0-1.48,1.23-2.65,2.7-2.65h3.69c1.47,0,2.66-1.19,2.66-2.65V3.88C16.99,1.83,14.67,1.48,12.06,1.48z M10.22,2.83 c0.41,0,0.73,0.33,0.73,0.74c0,0.41-0.33,0.74-0.73,0.74c-0.4,0-0.73-0.33-0.73-0.74C9.49,3.16,9.82,2.83,10.22,2.83z M16.71,9.89 v1.62c0,1.48-1.23,2.65-2.7,2.65H10.3c-1.47,0-2.66,1.19-2.66,2.65v1.49c0,2.05,2.32,2.41,4.92,2.41c3.14,0,3.52-0.67,3.52-0.67 l0.01-2.44h-3.63v-0.52h4.73c2.31,0,2.93-1.47,2.93-3.7c0-2.34-0.38-3.48-2.3-3.48H16.71z M13.88,18.96c0.41,0,0.73,0.33,0.73,0.74c0,0.41-0.33,0.74-0.73,0.74c-0.4,0-0.73-0.33-0.73-0.74C13.15,19.29,13.48,18.96,13.88,18.96z", + "close": "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z", + "engine": "M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9-2-2-5-2.4-7.4-1.3L9 6 6 9 1.6 4.6C.4 7 1 10 3 12c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.4-.4.4-1.1 0-1.3z", + "bolt": "M11 21h-1l1-7H7.5c-.58 0-.57-.32-.38-.66s.06-.11.08-.15C8.22 11.23 10.3 7.6 13.43 2.15c.18-.32.37-.15.37.15l-1 7h3.5c.58 0 .57.32.38.66s-.06.12-.08.16L11 21z", + "link": "M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z", } + def load_config(retry=True): config = configparser.ConfigParser() default_mirror = "https://pypi.org/simple" default_backup = "https://test.pypi.org/simple" - + if I18N.detect_system_language() == "zh_CN": default_mirror = "https://pypi.tuna.tsinghua.edu.cn/simple" default_backup = "https://mirrors.aliyun.com/pypi/simple/" if not os.path.exists(CONFIG_FILE): - config['Mappings'] = DEFAULT_MAPPINGS - config['Settings'] = { - 'language': 'auto', - 'engine': 'PyInstaller', - 'pip_index': default_mirror, - 'pip_index_backup': default_backup, - 'onefile': 'True', 'noconsole': 'True', 'clean_all': 'True', - 'auto_icon': 'True', 'use_venv': 'True', 'use_reqs': 'True', - 'use_pipreqs': 'True', 'use_pipreqs_dir': 'False', 'upx': 'False', 'concise_log': 'True', - 'cpu_cores': str(os.cpu_count() or 2), 'upx_path': '', - 'exclude_modules': '', 'out_mode': '0', 'custom_out_dir': '', - 'temp_sandbox_mode': '0', - 'sound_notify': 'True', 'auto_save_log': 'False', - 'use_reqs_file': '', 'add_data_list': '', 'custom_python_path': '', - 'pyi_version': '6.21.0', 'nuitka_version': '4.1.3', - 'enable_backport_shield': 'True', - 'lite_mode': 'False' + config["Mappings"] = DEFAULT_MAPPINGS + config["Settings"] = { + "language": "auto", + "engine": "PyInstaller", + "pip_index": default_mirror, + "pip_index_backup": default_backup, + "onefile": "True", + "noconsole": "True", + "clean_all": "True", + "auto_icon": "True", + "use_venv": "True", + "use_reqs": "True", + "use_pipreqs": "True", + "use_pipreqs_dir": "False", + "upx": "False", + "concise_log": "True", + "cpu_cores": str(os.cpu_count() or 2), + "upx_path": "", + "exclude_modules": "", + "out_mode": "0", + "custom_out_dir": "", + "temp_sandbox_mode": "0", + "sound_notify": "True", + "auto_save_log": "False", + "use_reqs_file": "", + "add_data_list": "", + "custom_python_path": "", + "pyi_version": "6.21.0", + "nuitka_version": "4.1.3", + "enable_backport_shield": "True", + "lite_mode": "False", } try: - with open(CONFIG_FILE, 'w', encoding='utf-8') as f: + with open(CONFIG_FILE, "w", encoding="utf-8") as f: config.write(f) - except: pass + except Exception: + pass else: try: - config.read(CONFIG_FILE, encoding='utf-8') + config.read(CONFIG_FILE, encoding="utf-8") except Exception: if retry: try: @@ -810,84 +937,96 @@ def load_config(retry=True): pass return load_config(retry=False) else: - if 'Mappings' not in config: config['Mappings'] = DEFAULT_MAPPINGS - if 'Settings' not in config: config['Settings'] = {} - if 'BackportRules' not in config: config['BackportRules'] = DEFAULT_BACKPORT_RULES + if "Mappings" not in config: + config["Mappings"] = DEFAULT_MAPPINGS + if "Settings" not in config: + config["Settings"] = {} + if "BackportRules" not in config: + config["BackportRules"] = DEFAULT_BACKPORT_RULES - if 'Mappings' not in config: - config['Mappings'] = DEFAULT_MAPPINGS + if "Mappings" not in config: + config["Mappings"] = DEFAULT_MAPPINGS else: updated_map = False for k, v in DEFAULT_MAPPINGS.items(): - if k not in config['Mappings']: - config['Mappings'][k] = v + if k not in config["Mappings"]: + config["Mappings"][k] = v updated_map = True if updated_map: - try: save_config(config) - except: pass + try: + save_config(config) + except Exception: + pass + + if "Settings" not in config: + config["Settings"] = {} - if 'Settings' not in config: config['Settings'] = {} - updated = False default_updates = { - 'language': 'auto', - 'keep_venv': 'False', - 'enable_sign': 'True' if sys.platform == 'darwin' else 'False', - 'cert_path': '', - 'cert_pass': '', - 'pip_index': default_mirror, - 'pip_index_backup': default_backup, - 'concise_log': 'True', - 'cpu_cores': str(os.cpu_count() or 2), - 'upx_path': '', - 'exclude_modules': '', - 'out_mode': '0', - 'custom_out_dir': '', - 'temp_sandbox_mode': '0', - 'sound_notify': 'True', - 'auto_save_log': 'False', - 'use_reqs_file': '', - 'add_data_list': '', - 'custom_python_path': '', - 'pyi_version': '6.21.0', - 'nuitka_version': '4.1.3', - 'venv_mode': 'isolated', - 'shared_venv_dir': '', - 'lite_mode': 'False' + "language": "auto", + "keep_venv": "False", + "enable_sign": "True" if sys.platform == "darwin" else "False", + "cert_path": "", + "cert_pass": "", + "pip_index": default_mirror, + "pip_index_backup": default_backup, + "concise_log": "True", + "cpu_cores": str(os.cpu_count() or 2), + "upx_path": "", + "exclude_modules": "", + "out_mode": "0", + "custom_out_dir": "", + "temp_sandbox_mode": "0", + "sound_notify": "True", + "auto_save_log": "False", + "use_reqs_file": "", + "add_data_list": "", + "custom_python_path": "", + "pyi_version": "6.21.0", + "nuitka_version": "4.1.3", + "venv_mode": "isolated", + "shared_venv_dir": "", + "lite_mode": "False", } for k, v in default_updates.items(): - if k not in config['Settings']: - config['Settings'][k] = v + if k not in config["Settings"]: + config["Settings"][k] = v updated = True - + if updated: - try: save_config(config) - except: pass - - if 'BackportRules' not in config: - config['BackportRules'] = DEFAULT_BACKPORT_RULES + try: + save_config(config) + except Exception: + pass + + if "BackportRules" not in config: + config["BackportRules"] = DEFAULT_BACKPORT_RULES else: updated_rules = False for k, v in DEFAULT_BACKPORT_RULES.items(): - if k not in config['BackportRules']: - config['BackportRules'][k] = v + if k not in config["BackportRules"]: + config["BackportRules"][k] = v updated_rules = True if updated_rules: - try: save_config(config) - except Exception: pass - lang_pref = config['Settings'].get('language', 'auto') + try: + save_config(config) + except Exception: + pass + lang_pref = config["Settings"].get("language", "auto") I18N.set_language(lang_pref) return config + def save_config(config): import time + temp_file = CONFIG_FILE + ".tmp" try: - with open(temp_file, 'w', encoding='utf-8') as f: + with open(temp_file, "w", encoding="utf-8") as f: config.write(f) except Exception: return - + for _ in range(5): try: if os.path.exists(CONFIG_FILE): @@ -900,6 +1039,7 @@ def save_config(config): except Exception: break + def create_pleasant_audio_files(): success_wav = _CONFIG_DIR / "sound_success.wav" failure_wav = _CONFIG_DIR / "sound_failure.wav" @@ -917,21 +1057,25 @@ def create_pleasant_audio_files(): dur_samples = int(dur * sample_rate) for i in range(dur_samples): idx = start_idx + i - if idx >= n_samples: break + if idx >= n_samples: + break t = i / sample_rate env = math.sin(math.pi * min(1.0, t / 0.025)) * math.exp(-4.2 * (t / dur)) - val = (math.sin(2 * math.pi * freq * t) * 0.75 + - math.sin(2 * math.pi * freq * 2 * t) * 0.2 + - math.sin(2 * math.pi * freq * 3 * t) * 0.05) * env + val = ( + math.sin(2 * math.pi * freq * t) * 0.75 + + math.sin(2 * math.pi * freq * 2 * t) * 0.2 + + math.sin(2 * math.pi * freq * 3 * t) * 0.05 + ) * env samples[idx] += val * 0.28 - with wave.open(success_wav.as_posix(), 'w') as wf: + with wave.open(success_wav.as_posix(), "w") as wf: wf.setnchannels(1) wf.setsampwidth(2) wf.setframerate(sample_rate) - packed = b''.join(struct.pack('= n_samples: break + if idx >= n_samples: + break t = i / sample_rate env = math.sin(math.pi * min(1.0, t / 0.02)) * math.exp(-3.2 * (t / dur)) - val = (math.sin(2 * math.pi * freq * t) * 0.8 + - math.sin(2 * math.pi * freq * 2 * t) * 0.2) * env + val = (math.sin(2 * math.pi * freq * t) * 0.8 + math.sin(2 * math.pi * freq * 2 * t) * 0.2) * env samples[idx] += val * 0.32 - with wave.open(failure_wav.as_posix(), 'w') as wf: + with wave.open(failure_wav.as_posix(), "w") as wf: wf.setnchannels(1) wf.setsampwidth(2) wf.setframerate(sample_rate) - packed = b''.join(struct.pack(' bool: path_str = path_obj.resolve().as_posix().lower() cloud_keywords = [ - 'onedrive', 'dropbox', 'icloud', 'google drive', 'googledrive', 'gdrive', - 'box sync', 'box.com', 'pcloud', 'mega', 'megasync', 'nextcloud', 'owncloud', - 'synology', 'seafile', 'tresorit', 'yandex', - 'nutstore', '坚果云', '百度云', '百度网盘', '阿里云盘', 'aliyun', '115', '微云', - 'quark', '夸克网盘', '夸克' + "onedrive", + "dropbox", + "icloud", + "google drive", + "googledrive", + "gdrive", + "box sync", + "box.com", + "pcloud", + "mega", + "megasync", + "nextcloud", + "owncloud", + "synology", + "seafile", + "tresorit", + "yandex", + "nutstore", + "坚果", + "百度", + "百度网盘", + "阿里云盘", + "aliyun", + "115", + "", + "quark", + "夸克网盘", + "夸克", ] return any(kw in path_str for kw in cloud_keywords) + def extract_project_imports_via_ast(target_path, scan_dir: bool = False) -> set: imports = set() target_path = Path(target_path) - + if not scan_dir or not target_path.is_dir(): files_to_scan = [target_path] if target_path.is_file() else [] else: files_to_scan = [] for root, _, files in os.walk(target_path): path_parts = set(Path(root).parts) - if path_parts & {'__pycache__', '.qpypack_build', '.qpypack_venv', '.venv', 'venv', 'build', 'dist', 'env', '.env'}: + if path_parts & {"__pycache__", ".qpypack_build", ".qpypack_venv", ".venv", "venv", "build", "dist", "env", ".env"}: continue for file in files: - if file.endswith(('.py', '.pyw')): + if file.endswith((".py", ".pyw")): files_to_scan.append(Path(root) / file) for file_p in files_to_scan: @@ -1024,17 +1198,18 @@ def extract_project_imports_via_ast(target_path, scan_dir: bool = False) -> set: tree = ast.parse(code, filename=file_p.as_posix()) for node in ast.walk(tree): if isinstance(node, ast.Import): - imports.update(n.name.split('.')[0] for n in node.names) + imports.update(n.name.split(".")[0] for n in node.names) elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module: - imports.add(node.module.split('.')[0]) + imports.add(node.module.split(".")[0]) except Exception: pass return imports + def query_target_env_packages(python_exe: str) -> dict: if not python_exe or not os.path.exists(python_exe): return {} - + code = ( "import json\n" "try:\n" @@ -1052,9 +1227,9 @@ def query_target_env_packages(python_exe: str) -> dict: clean_env.pop("PYTHONHOME", None) clean_env.pop("PYTHONPATH", None) kwargs = {"capture_output": True, "text": True, "env": clean_env, "timeout": 8, "errors": "ignore"} - if os.name == 'nt': + if os.name == "nt": kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW - + proc = subprocess.run([python_exe, "-c", code], **kwargs) if proc.returncode == 0 and proc.stdout.strip(): return json.loads(proc.stdout.strip()) @@ -1062,12 +1237,14 @@ def query_target_env_packages(python_exe: str) -> dict: pass return {} + def find_system_python(): candidates = [] - for name in ('python', 'python3', 'pythonw'): + for name in ("python", "python3", "pythonw"): p = shutil.which(name) - if p: candidates.append(p) - + if p: + candidates.append(p) + if winreg is not None: try: py = shutil.which("py") @@ -1077,14 +1254,22 @@ def find_system_python(): clean_env.pop("PYTHONPATH", None) clean_env["PYTHONUTF8"] = "1" proc = subprocess.Popen( - [py, "-c", "import sys; print(sys.executable)"], - stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, - text=True, encoding="utf-8", errors="ignore", env=clean_env, creationflags=subprocess.CREATE_NO_WINDOW + [py, "-c", "import sys; print(sys.executable)"], + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, + text=True, + encoding="utf-8", + errors="ignore", + env=clean_env, + creationflags=subprocess.CREATE_NO_WINDOW, ) out, _err = proc.communicate(timeout=3) - if out and os.path.exists(out.strip()): candidates.append(out.strip()) - except: pass - + if out and os.path.exists(out.strip()): + candidates.append(out.strip()) + except Exception: + pass + try: for hive in (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE): try: @@ -1095,16 +1280,22 @@ def find_system_python(): with winreg.OpenKey(core_key, rf"{sub}\InstallPath") as pkey: path = winreg.QueryValueEx(pkey, "")[0] exe = os.path.join(path, "python.exe") - if os.path.exists(exe): candidates.append(exe) - except: pass - except: pass - except: pass - + if os.path.exists(exe): + candidates.append(exe) + except Exception: + pass + except Exception: + pass + except Exception: + pass + search_paths = [ os.environ.get("LOCALAPPDATA", "") + r"\Programs\Python", os.environ.get("PROGRAMFILES", "") + r"\Python", os.environ.get("PROGRAMFILES(X86)", "") + r"\Python", - r"C:\Python", r"C:\Program Files\Python", r"C:\Program Files (x86)\Python" + r"C:\Python", + r"C:\Program Files\Python", + r"C:\Program Files (x86)\Python", ] for base in search_paths: if base and os.path.exists(base): @@ -1112,20 +1303,26 @@ def find_system_python(): for d in os.listdir(base): if d.lower().startswith("python"): exe = os.path.join(base, d, "python.exe") - if os.path.exists(exe): candidates.append(exe) - except: pass - + if os.path.exists(exe): + candidates.append(exe) + except Exception: + pass + user_profile = os.environ.get("USERPROFILE", "") if user_profile: for c_dir in ("miniconda3", "anaconda3", "Miniconda3", "Anaconda3"): for base in (user_profile, "C:\\", "D:\\"): exe = os.path.join(base, c_dir, "python.exe") - if os.path.exists(exe): candidates.append(exe) + if os.path.exists(exe): + candidates.append(exe) else: unix_bases = [ - "/usr/bin", "/usr/local/bin", "/opt/homebrew/bin", + "/usr/bin", + "/usr/local/bin", + "/opt/homebrew/bin", "/Library/Frameworks/Python.framework/Versions/Current/bin", - os.path.expanduser("~/.pyenv/shims"), os.path.expanduser("~/.local/bin") + os.path.expanduser("~/.pyenv/shims"), + os.path.expanduser("~/.local/bin"), ] user_profile = os.environ.get("HOME", "") or os.environ.get("USERPROFILE", "") if user_profile: @@ -1133,14 +1330,17 @@ def find_system_python(): base_dir = os.path.join(user_profile, c_dir) if os.path.exists(base_dir): exe = os.path.join(base_dir, "bin", "python") - if os.path.exists(exe): unix_bases.append(os.path.join(base_dir, "bin")) + if os.path.exists(exe): + unix_bases.append(os.path.join(base_dir, "bin")) envs_dir = os.path.join(base_dir, "envs") if os.path.exists(envs_dir): try: for env in os.listdir(envs_dir): p_bin = os.path.join(envs_dir, env, "bin") - if os.path.exists(p_bin): unix_bases.append(p_bin) - except: pass + if os.path.exists(p_bin): + unix_bases.append(p_bin) + except Exception: + pass for base in unix_bases: if os.path.exists(base): @@ -1148,8 +1348,10 @@ def find_system_python(): for f in os.listdir(base): if f.startswith("python3") or f == "python": exe = os.path.join(base, f) - if os.path.isfile(exe) and os.access(exe, os.X_OK): candidates.append(exe) - except: pass + if os.path.isfile(exe) and os.access(exe, os.X_OK): + candidates.append(exe) + except Exception: + pass seen = set() unique_candidates = [] @@ -1160,64 +1362,83 @@ def find_system_python(): unique_candidates.append(cand) for cand in unique_candidates: - if not os.path.exists(cand): continue - - if getattr(sys, 'frozen', False) or '__compiled__' in globals(): + if not os.path.exists(cand): + continue + + if getattr(sys, "frozen", False) or "__compiled__" in globals(): try: if os.path.samefile(cand, sys.executable): continue except Exception: pass - if os.name == 'nt' and "WindowsApps" in cand: + if os.name == "nt" and "WindowsApps" in cand: try: - if os.path.getsize(cand) == 0: continue - except: continue - + if os.path.getsize(cand) == 0: + continue + except Exception: + continue + clean_env = os.environ.copy() clean_env.pop("PYTHONHOME", None) clean_env.pop("PYTHONPATH", None) - + try: - kwargs = {"stdout": subprocess.PIPE, "stderr": subprocess.PIPE, "text": True, "encoding": "utf-8", "errors": "ignore", "env": clean_env, "timeout": 3} - if os.name == 'nt': kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW + kwargs = { + "stdout": subprocess.PIPE, + "stderr": subprocess.PIPE, + "text": True, + "encoding": "utf-8", + "errors": "ignore", + "env": clean_env, + "timeout": 3, + } + if os.name == "nt": + kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW proc = subprocess.run([cand, "-V"], **kwargs) - if proc.returncode == 0: return cand - except: continue - + if proc.returncode == 0: + return cand + except Exception: + continue + return "" + def get_svg_icon(name, color="#5F6368", size=24): path_data = MATERIAL_ICONS.get(name, "") svg_str = f'' renderer = QSvgRenderer() - renderer.load(svg_str.encode('utf-8')) - + renderer.load(svg_str.encode("utf-8")) + render_size = max(size * 4, 128) pixmap = QPixmap(render_size, render_size) pixmap.fill(Qt.GlobalColor.transparent) - + painter = QPainter(pixmap) painter.setRenderHint(QPainter.RenderHint.Antialiasing) painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) renderer.render(painter) painter.end() - + return QIcon(pixmap) + def get_svg_pixmap(name, color="#5F6368", size=64): return get_svg_icon(name, color, size).pixmap(size, size) + _ARROW_ICON_PATH = (_CONFIG_DIR / "dropdown_arrow.png").as_posix() + def ensure_arrow_icon(): if not os.path.exists(_ARROW_ICON_PATH): try: - pix = get_svg_pixmap('expand_more', color="#5F6368", size=32) + pix = get_svg_pixmap("expand_more", color="#5F6368", size=32) pix.save(_ARROW_ICON_PATH, "PNG") except Exception: pass + class CustomInputDialog(QDialog): def __init__(self, parent, title, label_text, text=""): super().__init__(parent) @@ -1233,35 +1454,36 @@ class CustomInputDialog(QDialog): QPushButton#btnOk { background-color: #2563eb; color: #ffffff; border: none; } QPushButton#btnOk:hover { background-color: #1d4ed8; } """) - + lay = QVBoxLayout(self) lay.setSpacing(10) lay.setContentsMargins(20, 20, 20, 20) - + lay.addWidget(QLabel(label_text)) - + self.edit = QLineEdit(text) lay.addWidget(self.edit) - + lay.addSpacing(10) - + btn_lay = QHBoxLayout() btn_lay.addStretch() - + self.btn_ok = QPushButton(_("OK")) self.btn_ok.setObjectName("btnOk") self.btn_ok.clicked.connect(self.accept) - + self.btn_cancel = QPushButton(_("Cancel")) self.btn_cancel.clicked.connect(self.reject) - + btn_lay.addWidget(self.btn_ok) btn_lay.addWidget(self.btn_cancel) lay.addLayout(btn_lay) - + def get_text(self): return self.edit.text() + class GenCertDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) @@ -1277,72 +1499,76 @@ class GenCertDialog(QDialog): QPushButton#btnOk { background-color: #2563eb; color: #ffffff; border: none; } QPushButton#btnOk:hover { background-color: #1d4ed8; } """) - + lay = QVBoxLayout(self) lay.setSpacing(10) lay.setContentsMargins(20, 20, 20, 20) - + form_lay = QFormLayout() - + self.subject_edit = QLineEdit(_("QPyPack Developer")) self.pass_edit = QLineEdit() self.pass_edit.setEchoMode(QLineEdit.EchoMode.Password) self.confirm_edit = QLineEdit() self.confirm_edit.setEchoMode(QLineEdit.EchoMode.Password) - + form_lay.addRow(QLabel(_("Certificate Subject (e.g. My Company):")), self.subject_edit) form_lay.addRow(QLabel(_("Password:")), self.pass_edit) form_lay.addRow(QLabel(_("Confirm Password:")), self.confirm_edit) - + lay.addLayout(form_lay) - - self.hint_label = QLabel(_("Recommendation: 6+ chars, alphanumeric, '-' and '_' only. Avoid spaces or Chinese to prevent signing errors.")) + + self.hint_label = QLabel( + _("Recommendation: 6+ chars, alphanumeric, '-' and '_' only. Avoid spaces or Chinese to prevent signing errors.") + ) self.hint_label.setWordWrap(True) self.hint_label.setStyleSheet("color: #64748b; font-size: 11px; margin-top: 2px;") lay.addWidget(self.hint_label) - + lay.addSpacing(10) - + btn_lay = QHBoxLayout() btn_lay.addStretch() - + self.btn_ok = QPushButton(_("Generate")) self.btn_ok.setObjectName("btnOk") self.btn_ok.clicked.connect(self.on_generate) - + self.btn_cancel = QPushButton(_("Cancel")) self.btn_cancel.clicked.connect(self.reject) - + btn_lay.addWidget(self.btn_ok) btn_lay.addWidget(self.btn_cancel) lay.addLayout(btn_lay) - + self.cert_info = None def on_generate(self): subject = self.subject_edit.text().strip() pwd = self.pass_edit.text() confirm = self.confirm_edit.text() - + if not subject: return - + if not pwd: QMessageBox.warning(self, _("Error"), _("Password cannot be empty!")) return - + if pwd != confirm: QMessageBox.warning(self, _("Error"), _("Passwords do not match!")) return - + self.cert_info = (subject, pwd) self.accept() + class ComboItemDelegate(QStyledItemDelegate): def sizeHint(self, option, index): s = super().sizeHint(option, index) return QSize(s.width(), max(s.height() + 12, 30)) + class TableItemDelegate(QStyledItemDelegate): def createEditor(self, parent, option, index): editor = QLineEdit(parent) @@ -1362,14 +1588,15 @@ class TableItemDelegate(QStyledItemDelegate): """) return editor -def setup_combo_white_theme(combo: QComboBox, min_view_width: int = None): + +def setup_combo_white_theme(combo: QComboBox, min_view_width: int | None = None): ensure_arrow_icon() list_view = QListView(combo) combo.setView(list_view) combo.setItemDelegate(ComboItemDelegate(combo)) - + combo.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon) - + if min_view_width and combo.view(): combo.view().setMinimumWidth(min_view_width) combo.view().setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) @@ -1391,6 +1618,7 @@ def setup_combo_white_theme(combo: QComboBox, min_view_width: int = None): combo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) combo.setMinimumWidth(150) + class DropListWidget(QListWidget): itemsDropped = Signal(list) @@ -1399,123 +1627,323 @@ class DropListWidget(QListWidget): self.setAcceptDrops(True) def dragEnterEvent(self, event: QDragEnterEvent): - if event.mimeData().hasUrls(): event.acceptProposedAction() + if event.mimeData().hasUrls(): + event.acceptProposedAction() def dragMoveEvent(self, event): - if event.mimeData().hasUrls(): event.acceptProposedAction() + if event.mimeData().hasUrls(): + event.acceptProposedAction() def dropEvent(self, event: QDropEvent): urls = event.mimeData().urls() if urls: paths = [u.toLocalFile() for u in urls if u.toLocalFile()] - if paths: self.itemsDropped.emit(paths) + if paths: + self.itemsDropped.emit(paths) + def get_stdlib_names(): libs = set(sys.builtin_module_names) - - if hasattr(sys, 'stdlib_module_names'): + + if hasattr(sys, "stdlib_module_names"): libs.update(sys.stdlib_module_names) - + try: import sysconfig - stdlib_dir = sysconfig.get_path('stdlib') + + stdlib_dir = sysconfig.get_path("stdlib") if stdlib_dir and os.path.exists(stdlib_dir): for f in os.listdir(stdlib_dir): - if f.endswith('.py'): + if f.endswith(".py"): libs.add(f[:-3]) - elif os.path.isdir(os.path.join(stdlib_dir, f)) and os.path.exists(os.path.join(stdlib_dir, f, '__init__.py')): + elif os.path.isdir(os.path.join(stdlib_dir, f)) and os.path.exists(os.path.join(stdlib_dir, f, "__init__.py")): libs.add(f) except Exception: pass known_std = { - 'abc', 'aifc', 'argparse', 'array', 'ast', 'asynchat', 'asyncio', 'asyncore', 'atexit', - 'audioop', 'base64', 'bdb', 'binascii', 'binhex', 'bisect', 'builtins', 'bz2', - 'calendar', 'cgi', 'cgitb', 'chunk', 'cmap', 'cmath', 'cmd', 'code', 'codecs', 'codeop', - 'collections', 'colorsys', 'compileall', 'concurrent', 'configparser', 'contextlib', - 'contextvars', 'copy', 'copyreg', 'crypt', 'csv', 'ctypes', 'curses', 'dataclasses', - 'datetime', 'dbm', 'decimal', 'difflib', 'dis', 'distutils', 'doctest', 'dummy_threading', - 'email', 'encodings', 'ensurepip', 'enum', 'errno', 'faulthandler', 'fcntl', 'filecmp', - 'fileinput', 'fnmatch', 'formatter', 'fractions', 'ftplib', 'functools', 'gc', 'genericpath', - 'getopt', 'getpass', 'gettext', 'glob', 'graphlib', 'grp', 'gzip', 'hashlib', 'heapq', - 'hmac', 'html', 'http', 'imaplib', 'imghdr', 'imp', 'importlib', 'inspect', 'io', - 'ipaddress', 'itertools', 'json', 'keyword', 'lib2to3', 'linecache', 'locale', 'logging', - 'lzma', 'macpath', 'mailbox', 'mailcap', 'marshal', 'math', 'mimetypes', 'mmap', 'modulefinder', - 'msvcrt', 'multiprocessing', 'netrc', 'nis', 'nntplib', 'numbers', 'opcode', 'operator', - 'optparse', 'os', 'ossaudiodev', 'pathlib', 'pdb', 'pickle', 'pickletools', 'pipes', - 'pkgutil', 'platform', 'plistlib', 'poplib', 'posix', 'posixpath', 'pprint', 'profile', - 'pstats', 'pty', 'pwd', 'py_compile', 'pyclbr', 'pydoc', 'queue', 'quopri', 'random', - 're', 'readline', 'reprlib', 'resource', 'rlcompleter', 'runpy', 'sched', 'secrets', - 'select', 'selectors', 'shelve', 'shlex', 'shutil', 'signal', 'site', 'smtpd', 'smtplib', - 'sndhdr', 'socket', 'socketserver', 'spwd', 'sqlite3', 'ssl', 'stat', 'statistics', - 'string', 'stringprep', 'struct', 'subprocess', 'sunau', 'symbol', 'symtable', 'sys', - 'sysconfig', 'syslog', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'termios', 'textwrap', - 'threading', 'time', 'timeit', 'tkinter', 'token', 'tokenize', 'tomllib', 'trace', - 'traceback', 'tracemalloc', 'tty', 'turtle', 'turtledemo', 'types', 'typing', 'unicodedata', 'unittest', - 'urllib', 'uu', 'uuid', 'venv', 'warnings', 'wave', 'weakref', 'webbrowser', 'winreg', 'winsound', - 'wsgiref', 'xdrlib', 'xml', 'xmlrpc', 'zipapp', 'zipfile', 'zipimport', 'zlib', 'zoneinfo', - '__future__', '_thread', '_asyncio' + "abc", + "aifc", + "argparse", + "array", + "ast", + "asynchat", + "asyncio", + "asyncore", + "atexit", + "audioop", + "base64", + "bdb", + "binascii", + "binhex", + "bisect", + "builtins", + "bz2", + "calendar", + "cgi", + "cgitb", + "chunk", + "cmap", + "cmath", + "cmd", + "code", + "codecs", + "codeop", + "collections", + "colorsys", + "compileall", + "concurrent", + "configparser", + "contextlib", + "contextvars", + "copy", + "copyreg", + "crypt", + "csv", + "ctypes", + "curses", + "dataclasses", + "datetime", + "dbm", + "decimal", + "difflib", + "dis", + "distutils", + "doctest", + "dummy_threading", + "email", + "encodings", + "ensurepip", + "enum", + "errno", + "faulthandler", + "fcntl", + "filecmp", + "fileinput", + "fnmatch", + "formatter", + "fractions", + "ftplib", + "functools", + "gc", + "genericpath", + "getopt", + "getpass", + "gettext", + "glob", + "graphlib", + "grp", + "gzip", + "hashlib", + "heapq", + "hmac", + "html", + "http", + "imaplib", + "imghdr", + "imp", + "importlib", + "inspect", + "io", + "ipaddress", + "itertools", + "json", + "keyword", + "lib2to3", + "linecache", + "locale", + "logging", + "lzma", + "macpath", + "mailbox", + "mailcap", + "marshal", + "math", + "mimetypes", + "mmap", + "modulefinder", + "msvcrt", + "multiprocessing", + "netrc", + "nis", + "nntplib", + "numbers", + "opcode", + "operator", + "optparse", + "os", + "ossaudiodev", + "pathlib", + "pdb", + "pickle", + "pickletools", + "pipes", + "pkgutil", + "platform", + "plistlib", + "poplib", + "posix", + "posixpath", + "pprint", + "profile", + "pstats", + "pty", + "pwd", + "py_compile", + "pyclbr", + "pydoc", + "queue", + "quopri", + "random", + "re", + "readline", + "reprlib", + "resource", + "rlcompleter", + "runpy", + "sched", + "secrets", + "select", + "selectors", + "shelve", + "shlex", + "shutil", + "signal", + "site", + "smtpd", + "smtplib", + "sndhdr", + "socket", + "socketserver", + "spwd", + "sqlite3", + "ssl", + "stat", + "statistics", + "string", + "stringprep", + "struct", + "subprocess", + "sunau", + "symbol", + "symtable", + "sys", + "sysconfig", + "syslog", + "tabnanny", + "tarfile", + "telnetlib", + "tempfile", + "termios", + "textwrap", + "threading", + "time", + "timeit", + "tkinter", + "token", + "tokenize", + "tomllib", + "trace", + "traceback", + "tracemalloc", + "tty", + "turtle", + "turtledemo", + "types", + "typing", + "unicodedata", + "unittest", + "urllib", + "uu", + "uuid", + "venv", + "warnings", + "wave", + "weakref", + "webbrowser", + "winreg", + "winsound", + "wsgiref", + "xdrlib", + "xml", + "xmlrpc", + "zipapp", + "zipfile", + "zipimport", + "zlib", + "zoneinfo", + "__future__", + "_thread", + "_asyncio", } libs.update(known_std) return libs + STD_LIBS = get_stdlib_names() + def get_resource_path(relative_path): - if hasattr(sys, '_MEIPASS'): + if hasattr(sys, "_MEIPASS"): base_path = sys._MEIPASS - elif '__compiled__' in globals(): + elif "__compiled__" in globals(): base_path = os.path.dirname(os.path.abspath(__file__)) else: base_path = os.path.dirname(os.path.abspath(__file__)) return os.path.join(base_path, relative_path) + def get_python_executable(): try: config = load_config() - custom_path = config['Settings'].get('custom_python_path', '').strip() + custom_path = config["Settings"].get("custom_python_path", "").strip() if custom_path and os.path.exists(custom_path) and os.path.isfile(custom_path): return custom_path - except Exception: pass + except Exception: + pass - if getattr(sys, 'frozen', False) or '__compiled__' in globals(): + if getattr(sys, "frozen", False) or "__compiled__" in globals(): return find_system_python() - + exe_name = Path(sys.executable).name.lower() - if exe_name in ('python.exe', 'python3.exe', 'pythonw.exe', 'python', 'python3'): + if exe_name in ("python.exe", "python3.exe", "pythonw.exe", "python", "python3"): return sys.executable - + return find_system_python() + def safe_read_text(path: Path) -> str: try: raw = path.read_bytes() - for enc in ('utf-8-sig', 'utf-8', 'gbk', 'gb18030', 'big5', 'latin-1'): + for enc in ("utf-8-sig", "utf-8", "gbk", "gb18030", "big5", "latin-1"): try: return raw.decode(enc) except UnicodeDecodeError: continue - return raw.decode(locale.getpreferredencoding(), errors='ignore') + return raw.decode(locale.getpreferredencoding(), errors="ignore") except Exception: return "" + def remove_readonly(func, path, exc_info=None): - try: + try: os.chmod(path, stat.S_IWRITE) func(path) - except Exception: pass + except Exception: + pass + def robust_rmtree(path: Path, retries=15, delay=0.8): - if not path.exists(): + if not path.exists(): return True - + for attempt in range(retries): try: if sys.version_info >= (3, 12): shutil.rmtree(path, onexc=lambda func, p, exc: remove_readonly(func, p)) else: shutil.rmtree(path, onerror=remove_readonly) - if not path.exists(): + if not path.exists(): return True except PermissionError as e: if attempt == retries - 1: @@ -1526,20 +1954,22 @@ def robust_rmtree(path: Path, retries=15, delay=0.8): if attempt == retries - 1: return False time.sleep(delay) - + return False + def convert_image_to_format(src_path, dest_path, dest_format): src = Path(src_path).resolve() dst = Path(dest_path).resolve() fmt = dest_format.lower() - + try: from PIL import Image + img = Image.open(src.as_posix()) if fmt == "ico": ico_sizes = [(256, 256), (128, 128), (64, 64), (48, 48), (32, 32), (16, 16)] - resample_filter = getattr(Image.Resampling, 'LANCZOS', getattr(Image, 'LANCZOS', Image.BICUBIC)) + resample_filter = getattr(Image.Resampling, "LANCZOS", getattr(Image, "LANCZOS", Image.BICUBIC)) img.save(dst.as_posix(), format="ICO", sizes=ico_sizes, resample=resample_filter) return True elif fmt == "icns": @@ -1551,10 +1981,10 @@ def convert_image_to_format(src_path, dest_path, dest_format): try: img = QImage(src.as_posix()) if not img.isNull(): - if fmt == "ico": + if fmt == "ico": img = img.scaled(256, 256, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) - writer = QImageWriter(dst.as_posix(), fmt.upper().encode('utf-8')) - if writer.write(img): + writer = QImageWriter(dst.as_posix(), fmt.upper().encode("utf-8")) + if writer.write(img): return True except Exception: pass @@ -1569,45 +1999,54 @@ def convert_image_to_format(src_path, dest_path, dest_format): if img_scaled.save(temp_png.as_posix(), "PNG"): png_bytes = temp_png.read_bytes() temp_png.unlink(missing_ok=True) - elif src.suffix.lower() == '.png': + elif src.suffix.lower() == ".png": png_bytes = src.read_bytes() if png_bytes: - header = struct.pack(' 0.0 and self.pop_value < 1.0: + if hasattr(self, "pop_value") and self.pop_value > 0.0 and self.pop_value < 1.0: pop_scale = 1.0 + math.sin(self.pop_value * math.pi) * 0.22 - + ripple_radius = (self.current_size / 2.0) + self.pop_value * 35.0 alpha = int(180 * (1.0 - self.pop_value)) pen = QPen(QColor(26, 115, 232, alpha)) @@ -1801,27 +2247,27 @@ class TargetIconWidget(QWidget): painter.setPen(pen) painter.setBrush(Qt.BrushStyle.NoBrush) painter.drawEllipse(QPointF(center_x, icon_center_y), ripple_radius, ripple_radius) - + draw_size = int(self.current_size * pop_scale) - + if self.is_building: radius = (self.current_size / 2) + 12 pen = QPen(QColor(26, 115, 232, 200)) pen.setWidth(4) pen.setCapStyle(Qt.PenCapStyle.RoundCap) painter.setPen(pen) - + rect = QRectF(center_x - radius, center.y() - radius, radius * 2, radius * 2) span_angle = int((140 + 60 * math.sin(self.pulse_value * 1.5)) * 16) start_angle = int(-self.spin_angle * 16) painter.drawArc(rect, start_angle, span_angle) - + pix_rect = QRectF(center_x - draw_size / 2.0, icon_center_y - draw_size / 2.0, float(draw_size), float(draw_size)) scaled_pix = self.pixmap.scaled( - int(draw_size * self.devicePixelRatioF()), - int(draw_size * self.devicePixelRatioF()), - Qt.AspectRatioMode.KeepAspectRatio, - Qt.TransformationMode.SmoothTransformation + int(draw_size * self.devicePixelRatioF()), + int(draw_size * self.devicePixelRatioF()), + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation, ) scaled_pix.setDevicePixelRatio(self.devicePixelRatioF()) painter.drawPixmap(QPointF(pix_rect.x(), pix_rect.y()), scaled_pix) @@ -1842,12 +2288,13 @@ class TargetIconWidget(QWidget): self.pop_value = val self.update() + class DropArea(QFrame): fileDropped = Signal(str) def __init__(self, parent=None): super().__init__(parent) - self.setObjectName("DropArea") + self.setObjectName("DropArea") self.setAcceptDrops(True) self.setFrameStyle(QFrame.Shape.NoFrame) self.setCursor(Qt.CursorShape.PointingHandCursor) @@ -1863,33 +2310,34 @@ class DropArea(QFrame): icon_path = get_resource_path("icon.ico") if os.path.exists(icon_path): pixmap = QIcon(icon_path).pixmap(256, 256) - if not pixmap.isNull(): return pixmap - return get_svg_pixmap('python', color="#9AA0A6", size=256) + if not pixmap.isNull(): + return pixmap + return get_svg_pixmap("python", color="#9AA0A6", size=256) def init_ui(self): layout = QVBoxLayout(self) layout.setContentsMargins(10, 20, 10, 20) layout.setSpacing(0) layout.addStretch(1) - + self.icon_widget = TargetIconWidget(self) self.icon_widget.set_default_pixmap(self._get_default_pixmap(88)) - + h_layout = QHBoxLayout() h_layout.addStretch(1) h_layout.addWidget(self.icon_widget) h_layout.addStretch(1) layout.addLayout(h_layout) layout.addSpacing(18) - + self.label = QLabel(_("Python Packaging, Reimagined.")) self.label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.label.setWordWrap(True) self.label.setStyleSheet("QLabel { background: transparent; color: #5F6368; font-size: 16px; font-weight: bold; border: none; }") layout.addWidget(self.label) - + layout.addSpacing(8) - + self.sub_label = QLabel() self.sub_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.sub_label.setWordWrap(True) @@ -1902,7 +2350,7 @@ class DropArea(QFrame): self.chips_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(self.chips_container) layout.addStretch(1) - + self.retranslate_ui() def retranslate_ui(self): @@ -1916,39 +2364,44 @@ class DropArea(QFrame): self.setStyleSheet("#DropArea { background-color: #E8F0FE; border: 2px dashed #1A73E8; border-radius: 12px; }") def dragLeaveEvent(self, event): - self.setStyleSheet("#DropArea { background-color: #f8f9fa; border: 2px dashed #dadce0; border-radius: 12px; } #DropArea:hover { background-color: #f1f3f4; border: 2px dashed #bdc1c6; }") + self.setStyleSheet( + "#DropArea { background-color: #f8f9fa; border: 2px dashed #dadce0; border-radius: 12px; } #DropArea:hover { background-color: #f1f3f4; border: 2px dashed #bdc1c6; }" + ) def dropEvent(self, event: QDropEvent): self.dragLeaveEvent(event) urls = event.mimeData().urls() - if not urls: return - - paths = [u.toLocalFile() for u in urls if u.toLocalFile()] - if not paths: return + if not urls: + return + + paths = [u.toLocalFile() for u in urls if u.toLocalFile()] + if not paths: + return + + py_files = [p for p in paths if p.lower().endswith((".py", ".pyw"))] - py_files = [p for p in paths if p.lower().endswith(('.py', '.pyw'))] - if py_files: self.fileDropped.emit(py_files[0]) else: parent_win = self.window() - if hasattr(parent_win, 'on_main_resources_dropped'): + if hasattr(parent_win, "on_main_resources_dropped"): parent_win.on_main_resources_dropped(paths) def mousePressEvent(self, event): if event.button() == Qt.MouseButton.LeftButton: fp, _filter = QFileDialog.getOpenFileName( - self, - _("Drag & Drop Python script (.py/.pyw) here\nor Click to Browse"), - "", + self, + _("Drag & Drop Python script (.py/.pyw) here\nor Click to Browse"), + "", "Python Scripts (*.py *.pyw);;All Files (*)", - options=QFileDialog.Option.DontUseNativeDialog + options=QFileDialog.Option.DontUseNativeDialog, ) - if fp: self.fileDropped.emit(fp) + if fp: + self.fileDropped.emit(fp) def set_loading(self, filename): self.current_filename = filename - pixmap = get_svg_pixmap('package', color="#1A73E8", size=88) + pixmap = get_svg_pixmap("package", color="#1A73E8", size=88) self.icon_widget.set_file_pixmap(pixmap, 88) self.label.setText(_("Loaded: {filename}", filename=filename)) self.label.setStyleSheet("QLabel { background: transparent; color: #1A73E8; font-size: 16px; font-weight: bold; border: none; }") @@ -1959,11 +2412,12 @@ class DropArea(QFrame): pixmap = None if custom_icon_path and Path(custom_icon_path).exists(): pixmap = QIcon(str(custom_icon_path)).pixmap(256, 256) - if pixmap.isNull(): pixmap = None - + if pixmap.isNull(): + pixmap = None + if not pixmap: - pixmap = get_svg_pixmap('package', color="#1A73E8", size=256) - + pixmap = get_svg_pixmap("package", color="#1A73E8", size=256) + self.icon_widget.set_file_pixmap(pixmap, 88) self.label.setText(_("Loaded: {filename}", filename=filename)) self.label.setStyleSheet("QLabel { background: transparent; color: #1A73E8; font-size: 16px; font-weight: bold; border: none; }") @@ -1986,13 +2440,13 @@ class DropArea(QFrame): if not pix.isNull(): self.icon_widget.set_custom_pixmap(pix, size) pixmap_set = True - + if not pixmap_set: if self.icon_widget.base_pixmap and not self.icon_widget.base_pixmap.isNull(): self.icon_widget.set_custom_pixmap(self.icon_widget.base_pixmap, size) else: - self.icon_widget.set_custom_pixmap(get_svg_pixmap('check', color="#1E8E3E", size=size), size) - + self.icon_widget.set_custom_pixmap(get_svg_pixmap("check", color="#1E8E3E", size=size), size) + self.icon_widget.start_success() self.label.setText(_("Build Successful")) self.label.setStyleSheet("QLabel { background: transparent; color: #1E8E3E; font-size: 20px; font-weight: bold; border: none; }") @@ -2000,13 +2454,13 @@ class DropArea(QFrame): def show_failure(self): size = 128 - self.icon_widget.set_custom_pixmap(get_svg_pixmap('close', color="#D93025", size=size), size) + self.icon_widget.set_custom_pixmap(get_svg_pixmap("close", color="#D93025", size=size), size) self.icon_widget.start_failure() - + self.label.setText(_("Build Failed")) self.label.setStyleSheet("QLabel { background: transparent; color: #D93025; font-size: 20px; font-weight: bold; border: none; }") self.sub_label.setText(_("Check log output below for troubleshooting.")) - + def reset(self): self.current_filename = None self.icon_widget.reset() @@ -2018,7 +2472,7 @@ class DropArea(QFrame): item = self.chips_layout.takeAt(0) if item.widget(): item.widget().deleteLater() - + if not script_path and not add_data_list: return @@ -2028,11 +2482,11 @@ class DropArea(QFrame): if add_data_list: for item in add_data_list[:3]: - r_type, src, dst = item + r_type, src, _dst = item p = Path(src) - if r_type == 'dir': + if r_type == "dir": chips.append(("📁 " + p.name, "#f0fdf4", "#15803d", "#bbf7d0")) - elif p.suffix.lower() in ('.ico', '.png', '.jpg', '.jpeg', '.svg'): + elif p.suffix.lower() in (".ico", ".png", ".jpg", ".jpeg", ".svg"): chips.append(("🖼️ " + p.name, "#fef3c7", "#b45309", "#fde68a")) else: chips.append(("📄 " + p.name, "#f3f4f6", "#374151", "#e5e7eb")) @@ -2056,15 +2510,17 @@ class DropArea(QFrame): """) self.chips_layout.addWidget(chip) + class PythonScannerThread(QThread): scan_done = Signal(dict) - + def run(self): candidates = set() - for name in ('python', 'python3', 'pythonw'): + for name in ("python", "python3", "pythonw"): p = shutil.which(name) - if p: candidates.add(os.path.normpath(p)) - + if p: + candidates.add(os.path.normpath(p)) + if winreg is not None: try: for hive in (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE): @@ -2076,16 +2532,22 @@ class PythonScannerThread(QThread): with winreg.OpenKey(core_key, rf"{sub}\InstallPath") as pkey: path = winreg.QueryValueEx(pkey, "")[0] exe = os.path.join(path, "python.exe") - if os.path.exists(exe): candidates.add(os.path.normpath(exe)) - except: pass - except: pass - except: pass - + if os.path.exists(exe): + candidates.add(os.path.normpath(exe)) + except Exception: + pass + except Exception: + pass + except Exception: + pass + search_paths = [ os.environ.get("LOCALAPPDATA", "") + r"\Programs\Python", os.environ.get("PROGRAMFILES", "") + r"\Python", os.environ.get("PROGRAMFILES(X86)", "") + r"\Python", - r"C:\Python", r"C:\Program Files\Python", r"C:\Program Files (x86)\Python" + r"C:\Python", + r"C:\Program Files\Python", + r"C:\Program Files (x86)\Python", ] for base in search_paths: if base and os.path.exists(base): @@ -2093,9 +2555,11 @@ class PythonScannerThread(QThread): for d in os.listdir(base): if d.lower().startswith("python"): exe = os.path.join(base, d, "python.exe") - if os.path.exists(exe): candidates.add(os.path.normpath(exe)) - except: pass - + if os.path.exists(exe): + candidates.add(os.path.normpath(exe)) + except Exception: + pass + user_profile = os.environ.get("USERPROFILE", "") if user_profile: for c_dir in ("miniconda3", "anaconda3", "Miniconda3", "Anaconda3", ".conda"): @@ -2103,18 +2567,24 @@ class PythonScannerThread(QThread): base_dir = os.path.join(base, c_dir) if os.path.exists(base_dir): exe = os.path.join(base_dir, "python.exe") - if os.path.exists(exe): candidates.add(os.path.normpath(exe)) + if os.path.exists(exe): + candidates.add(os.path.normpath(exe)) envs_dir = os.path.join(base_dir, "envs") if os.path.exists(envs_dir): try: for env in os.listdir(envs_dir): exe = os.path.join(envs_dir, env, "python.exe") - if os.path.exists(exe): candidates.add(os.path.normpath(exe)) - except: pass + if os.path.exists(exe): + candidates.add(os.path.normpath(exe)) + except Exception: + pass else: unix_bases = [ - "/usr/bin", "/usr/local/bin", "/opt/homebrew/bin", - os.path.expanduser("~/.pyenv/shims"), os.path.expanduser("~/.local/bin") + "/usr/bin", + "/usr/local/bin", + "/opt/homebrew/bin", + os.path.expanduser("~/.pyenv/shims"), + os.path.expanduser("~/.local/bin"), ] user_profile = os.environ.get("HOME", "") or os.environ.get("USERPROFILE", "") if user_profile: @@ -2122,14 +2592,17 @@ class PythonScannerThread(QThread): base_dir = os.path.join(user_profile, c_dir) if os.path.exists(base_dir): exe = os.path.join(base_dir, "bin", "python") - if os.path.exists(exe): unix_bases.append(os.path.join(base_dir, "bin")) + if os.path.exists(exe): + unix_bases.append(os.path.join(base_dir, "bin")) envs_dir = os.path.join(base_dir, "envs") if os.path.exists(envs_dir): try: for env in os.listdir(envs_dir): p_bin = os.path.join(envs_dir, env, "bin") - if os.path.exists(p_bin): unix_bases.append(p_bin) - except: pass + if os.path.exists(p_bin): + unix_bases.append(p_bin) + except Exception: + pass for base in unix_bases: if os.path.exists(base): @@ -2139,7 +2612,8 @@ class PythonScannerThread(QThread): exe = os.path.join(base, f) if os.path.isfile(exe) and os.access(exe, os.X_OK): candidates.add(os.path.normpath(exe)) - except: pass + except Exception: + pass resolved_candidates = set() for cand in candidates: @@ -2150,35 +2624,47 @@ class PythonScannerThread(QThread): elif resolved_path.startswith("\\\\?\\"): resolved_path = resolved_path[4:] resolved_candidates.add(resolved_path) - except: + except Exception: resolved_candidates.add(os.path.normpath(cand)) valid_pythons = {} def check_candidate(cand): - if getattr(sys, 'frozen', False) or '__compiled__' in globals(): + if getattr(sys, "frozen", False) or "__compiled__" in globals(): try: if os.path.samefile(cand, sys.executable): return None except Exception: pass - if os.name == 'nt' and "WindowsApps" in cand: + if os.name == "nt" and "WindowsApps" in cand: try: - if os.path.getsize(cand) == 0: return None - except: return None + if os.path.getsize(cand) == 0: + return None + except Exception: + return None try: clean_env = os.environ.copy() clean_env.pop("PYTHONHOME", None) clean_env.pop("PYTHONPATH", None) clean_env["PYTHONUTF8"] = "1" clean_env["PYTHONIOENCODING"] = "utf-8" - kwargs = {"stdout": subprocess.PIPE, "stderr": subprocess.PIPE, "text": True, "encoding": "utf-8", "errors": "ignore", "env": clean_env, "timeout": 2} - if os.name == 'nt': kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW + kwargs = { + "stdout": subprocess.PIPE, + "stderr": subprocess.PIPE, + "text": True, + "encoding": "utf-8", + "errors": "ignore", + "env": clean_env, + "timeout": 2, + } + if os.name == "nt": + kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW proc = subprocess.run([cand, "-c", "import sys; print(sys.version.split()[0])"], **kwargs) if proc.returncode == 0: return (cand, proc.stdout.strip()) - except: pass + except Exception: + pass return None with ThreadPoolExecutor(max_workers=min(12, len(resolved_candidates) or 1)) as executor: @@ -2189,6 +2675,7 @@ class PythonScannerThread(QThread): self.scan_done.emit(valid_pythons) + class VenvCleanerTaskThread(QThread): scan_done = Signal(list, int) delete_done = Signal(int, float) @@ -2208,14 +2695,16 @@ class VenvCleanerTaskThread(QThread): for p in self.target_dir.iterdir(): if p.is_dir() and p.name.startswith(".qpypack_venv"): venvs.append(p) - except Exception: pass + except Exception: + pass for venv in venvs: try: - for f in venv.rglob('*'): + for f in venv.rglob("*"): if f.is_file(): freed_bytes += f.stat().st_size - except Exception: pass + except Exception: + pass self.scan_done.emit(venvs, freed_bytes) @@ -2225,9 +2714,11 @@ class VenvCleanerTaskThread(QThread): for venv in self.venvs_to_delete: v_bytes = 0 try: - for f in venv.rglob('*'): - if f.is_file(): v_bytes += f.stat().st_size - except Exception: pass + for f in venv.rglob("*"): + if f.is_file(): + v_bytes += f.stat().st_size + except Exception: + pass if robust_rmtree(venv): success_count += 1 @@ -2235,14 +2726,15 @@ class VenvCleanerTaskThread(QThread): self.delete_done.emit(success_count, total_bytes / (1024 * 1024)) + class SettingsPanel(QWidget): def __init__(self, parent=None): super().__init__(parent) self.parent_win = parent - self.upx_check = None + self.upx_check = None self.upx_path_container = None self.out_dir_container = None - + self.setStyleSheet(""" SettingsPanel { background-color: #f9fafb; } QLabel { color: #111827; font-size: 13px; font-weight: 600; background: transparent; } @@ -2289,7 +2781,7 @@ class SettingsPanel(QWidget): I18N.language_changed.connect(self.retranslate_ui) self.load_from_config() self.retranslate_ui() - if hasattr(self, 'btn_clean_venvs'): + if hasattr(self, "btn_clean_venvs"): self.btn_clean_venvs.setText(_("Clear Local Venvs")) self.scanner_thread = PythonScannerThread() self.scanner_thread.scan_done.connect(self.populate_python_combo) @@ -2313,15 +2805,15 @@ class SettingsPanel(QWidget): scroll.setWidgetResizable(True) scroll.setFrameShape(QFrame.Shape.NoFrame) scroll.setStyleSheet("QScrollArea { background: transparent; }") - + content = QWidget() content.setObjectName("ScrollContent") content.setStyleSheet("QWidget#ScrollContent { background: transparent; }") - + lay = QVBoxLayout(content) lay.setContentsMargins(0, 5, 0, 15) lay.setSpacing(15) - + scroll.setWidget(content) return scroll, content, lay @@ -2333,105 +2825,106 @@ class SettingsPanel(QWidget): idx = combo.currentIndex() if idx >= 0 and combo.currentText() == combo.itemText(idx): data = combo.itemData(idx) - if data: return data + if data: + return data text = combo.currentText().strip() - m = re.search(r'https?://[^\s]+', text) + m = re.search(r"https?://[^\s]+", text) if m: - return m.group(0).rstrip('/') - if not text.startswith(('http://', 'https://')): - return '' + return m.group(0).rstrip("/") + if not text.startswith(("http://", "https://")): + return "" return text def _set_combo_value(self, combo, url_val): - url_val = (url_val or '').strip() - if not url_val: return + url_val = (url_val or "").strip() + if not url_val: + return for i in range(combo.count()): data = combo.itemData(i) - if data and data.rstrip('/') == url_val.rstrip('/'): + if data and data.rstrip("/") == url_val.rstrip("/"): combo.setCurrentIndex(i) return combo.setCurrentText(url_val) def init_ui(self): layout = QVBoxLayout(self) - layout.setSpacing(15) + layout.setSpacing(15) layout.setContentsMargins(20, 10, 20, 20) - + self.tabs = QTabWidget() self.tabs.setObjectName("MainTabWidget") self.tabs.tabBar().setObjectName("MainTabBar") self.tabs.tabBar().setExpanding(False) - + self.tab_build = QWidget() self.tab_pref_scroll, _cnt_pref, self.lay_pref = self._create_scroll_tab() self.tab_about_scroll, _cnt_abt, self.lay_about = self._create_scroll_tab() - self.tabs.addTab(self.tab_build, get_svg_icon('package', "#5F6368", 16), _("Build Settings")) - self.tabs.addTab(self.tab_pref_scroll, get_svg_icon('settings', "#5F6368", 16), _("Preferences")) - self.tabs.addTab(self.tab_about_scroll, get_svg_icon('info', "#5F6368", 16), _("About")) + self.tabs.addTab(self.tab_build, get_svg_icon("package", "#5F6368", 16), _("Build Settings")) + self.tabs.addTab(self.tab_pref_scroll, get_svg_icon("settings", "#5F6368", 16), _("Preferences")) + self.tabs.addTab(self.tab_about_scroll, get_svg_icon("info", "#5F6368", 16), _("About")) - self.build_build_master_tab() self.build_pref_tab() self.build_about_tab() layout.addWidget(self.tabs) - + btn_lay = QHBoxLayout() btn_lay.setContentsMargins(0, 5, 0, 0) btn_lay.setSpacing(12) - + self.btn_reset = AnimatedButton("") self.btn_reset.setFixedSize(44, 44) - self.btn_reset.setIcon(get_svg_icon('refresh', "#5F6368")) + self.btn_reset.setIcon(get_svg_icon("refresh", "#5F6368")) self.btn_reset.setToolTip(_("Reset to Default Config")) self.btn_reset.setStyleSheet(self.parent_win.icon_btn_style) self.btn_reset.clicked.connect(self.parent_win.reset_all) btn_lay.addWidget(self.btn_reset) - + self.btn_save = AnimatedButton(_("Save & Return")) self.btn_save.setFixedHeight(44) self.btn_save.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) - self.btn_save.setIcon(get_svg_icon('check', "white")) + self.btn_save.setIcon(get_svg_icon("check", "white")) self.btn_save.setStyleSheet(self.parent_win.primary_btn_style) self.btn_save.clicked.connect(self.parent_win.save_settings_and_return) btn_lay.addWidget(self.btn_save) self.btn_back = AnimatedButton("") self.btn_back.setFixedSize(44, 44) - self.btn_back.setIcon(get_svg_icon('back', "#5F6368")) + self.btn_back.setIcon(get_svg_icon("back", "#5F6368")) self.btn_back.setToolTip(_("Cancel & Return")) self.btn_back.setStyleSheet(self.parent_win.icon_btn_style) self.btn_back.clicked.connect(self.parent_win.show_main) btn_lay.addWidget(self.btn_back) - + layout.addLayout(btn_lay) def build_build_master_tab(self): main_lay = QVBoxLayout(self.tab_build) main_lay.setContentsMargins(0, 10, 0, 0) - + self.sub_tabs = QTabWidget() self.sub_tabs.setObjectName("SubTabWidget") self.sub_tabs.tabBar().setObjectName("SubTabBar") self.sub_tabs.tabBar().setExpanding(False) - + sub_scroll1, _cnt1, lay_sub1 = self._create_scroll_tab() - sub_scroll2, _cnt2, lay_sub2 = self._create_scroll_tab() + sub_scroll2, _cnt2, lay_sub2 = self._create_scroll_tab() sub_scroll3, _cnt3, lay_sub3 = self._create_scroll_tab() sub_scroll4, _cnt4, lay_sub4 = self._create_scroll_tab() sub_scroll5, _cnt5, lay_sub5 = self._create_scroll_tab() - self.sub_tabs.addTab(sub_scroll1, get_svg_icon('engine', "#5F6368", 16), _("Engine")) - self.sub_tabs.addTab(sub_scroll2, get_svg_icon('package', "#5F6368", 16), _("Dependencies")) - self.sub_tabs.addTab(sub_scroll3, get_svg_icon('folder', "#5F6368", 16), _("Resources")) - self.sub_tabs.addTab(sub_scroll4, get_svg_icon('bolt', "#5F6368", 16), _("Optimization & Security")) - self.sub_tabs.addTab(sub_scroll5, get_svg_icon('link', "#5F6368", 16), _("Package Map")) + self.sub_tabs.addTab(sub_scroll1, get_svg_icon("engine", "#5F6368", 16), _("Engine")) + self.sub_tabs.addTab(sub_scroll2, get_svg_icon("package", "#5F6368", 16), _("Dependencies")) + self.sub_tabs.addTab(sub_scroll3, get_svg_icon("folder", "#5F6368", 16), _("Resources")) + self.sub_tabs.addTab(sub_scroll4, get_svg_icon("bolt", "#5F6368", 16), _("Optimization & Security")) + self.sub_tabs.addTab(sub_scroll5, get_svg_icon("link", "#5F6368", 16), _("Package Map")) self.card_engine, c_lay_engine = self._create_card(_("Engine & Environment")) self.form_engine = QFormLayout() self.form_engine.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) self.form_engine.setSpacing(15) - + self.engine_combo = QComboBox() self.engine_combo.addItems(["PyInstaller", "Nuitka"]) setup_combo_white_theme(self.engine_combo) @@ -2473,13 +2966,13 @@ class SettingsPanel(QWidget): lay_eng.setSpacing(4) lay_eng.addWidget(self.engine_combo) lay_eng.addWidget(self.engine_desc_lbl) - + self.python_path_combo = QComboBox() self.python_path_combo.setEditable(True) self.python_path_combo.setPlaceholderText(_("Leave blank to auto-detect system default Python")) setup_combo_white_theme(self.python_path_combo, min_view_width=520) self.python_path_combo.currentTextChanged.connect(self.on_python_path_changed) - + self.btn_python_path = QPushButton(_("Browse")) self.btn_python_path.setProperty("class", "ToolBtn") self.btn_python_path.clicked.connect(self.select_python_path) @@ -2488,7 +2981,7 @@ class SettingsPanel(QWidget): self.btn_download_py.setProperty("class", "ToolBtn") self.btn_download_py.setToolTip("") self.btn_download_py.clicked.connect(self.on_download_python_clicked) - + py_cont = QWidget() lay_py = QVBoxLayout(py_cont) lay_py.setContentsMargins(0, 0, 0, 0) @@ -2505,21 +2998,21 @@ class SettingsPanel(QWidget): self.name_edit = QLineEdit() self.name_edit.setPlaceholderText(_("Leave blank to auto-match script name")) - + self.icon_edit = QLineEdit() self.icon_preview = QLabel() self.icon_preview.setFixedSize(24, 24) self.icon_preview.setScaledContents(True) self.icon_edit.textChanged.connect(self.update_icon_preview) - + self.btn_icon = QPushButton(_("Browse")) self.btn_icon.setProperty("class", "ToolBtn") self.btn_icon.clicked.connect(self.select_icon) - + icon_cont = QWidget() h_icon = QHBoxLayout(icon_cont) - h_icon.setContentsMargins(0,0,0,0) + h_icon.setContentsMargins(0, 0, 0, 0) h_icon.addWidget(self.icon_edit, 1) h_icon.addWidget(self.icon_preview) h_icon.addWidget(self.btn_icon) @@ -2527,7 +3020,7 @@ class SettingsPanel(QWidget): self.rb_compat_mode = QRadioButton(_("Compatibility Mode (Default)")) self.rb_lite_mode = QRadioButton(_("Lite Mode (Smaller size)")) self.rb_compat_mode.setChecked(True) - + pack_mode_cont = QWidget() h_pack_mode = QHBoxLayout(pack_mode_cont) h_pack_mode.setContentsMargins(0, 0, 0, 0) @@ -2588,7 +3081,7 @@ class SettingsPanel(QWidget): self.form_deps = QFormLayout() self.form_deps.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) self.form_deps.setSpacing(15) - + self.pip_source_combo = QComboBox() self.pip_source_combo.setEditable(True) setup_combo_white_theme(self.pip_source_combo, min_view_width=520) @@ -2601,7 +3094,7 @@ class SettingsPanel(QWidget): display_text = f"{_(name)}: {url}" self.pip_source_combo.addItem(display_text, url) self.pip_backup_combo.addItem(display_text, url) - + self.pip_source_combo.currentTextChanged.connect(self._check_pip_mirrors) self.pip_backup_combo.currentTextChanged.connect(self._check_pip_mirrors) @@ -2612,25 +3105,25 @@ class SettingsPanel(QWidget): self.btn_reqs.clicked.connect(self.select_reqs_file) reqs_cont = QWidget() h_reqs = QHBoxLayout(reqs_cont) - h_reqs.setContentsMargins(0,0,0,0) + h_reqs.setContentsMargins(0, 0, 0, 0) h_reqs.addWidget(self.reqs_file_edit, 1) h_reqs.addWidget(self.btn_reqs) - + self.hidden_edit = QLineEdit() self.hidden_edit.setPlaceholderText(_("Comma separated (e.g. pandas, PyQt5)")) self.btn_scan = QPushButton(_("AST Scan")) self.btn_scan.setProperty("class", "ToolBtn") self.btn_scan.clicked.connect(self.auto_scan_hidden) - + hid_cont = QWidget() h_hid = QHBoxLayout(hid_cont) - h_hid.setContentsMargins(0,0,0,0) + h_hid.setContentsMargins(0, 0, 0, 0) h_hid.addWidget(self.hidden_edit, 1) h_hid.addWidget(self.btn_scan) self.exclude_edit = QLineEdit() self.exclude_edit.setPlaceholderText(_("Comma separated (e.g. tkinter, matplotlib)")) - + self.lbl_pip_main = QLabel(_("Primary PIP Index:")) self.lbl_pip_backup = QLabel(_("Backup PIP Index:")) self.lbl_reqs = QLabel(_("Requirements File:")) @@ -2643,7 +3136,7 @@ class SettingsPanel(QWidget): self.form_deps.addRow(self.lbl_hidden, hid_cont) self.form_deps.addRow(self.lbl_exclude, self.exclude_edit) c_lay_deps.addLayout(self.form_deps) - + c_lay_deps.addSpacing(5) g_dep = QGridLayout() g_dep.setSpacing(10) @@ -2700,7 +3193,7 @@ class SettingsPanel(QWidget): c_lay_deps.addLayout(g_dep) self.card_env, c_lay_env = self._create_card(_("Environment Analysis")) - + btn_env_lay = QHBoxLayout() self.btn_inspect_env = QPushButton(_("Inspect Interpreter Environment")) self.btn_inspect_env.setProperty("class", "ToolBtn") @@ -2724,38 +3217,40 @@ class SettingsPanel(QWidget): lay_sub2.addStretch() self.card_res, c_lay_res = self._create_card(_("Additional Resources (Drag & Drop Supported)")) - + self.lbl_res_hint = QLabel(_("Project-specific. Not saved to global preferences. Use 'Export Preset' to save config.")) - self.lbl_res_hint.setWordWrap(True) - self.lbl_res_hint.setStyleSheet("font-size: 12px; color: #6b7280; font-weight: normal; margin-top: -2px; margin-bottom: 6px; line-height: 1.4;") + self.lbl_res_hint.setWordWrap(True) + self.lbl_res_hint.setStyleSheet( + "font-size: 12px; color: #6b7280; font-weight: normal; margin-top: -2px; margin-bottom: 6px; line-height: 1.4;" + ) c_lay_res.addWidget(self.lbl_res_hint) self.add_data_list = DropListWidget() self.add_data_list.setSelectionMode(QListWidget.SelectionMode.ExtendedSelection) self.add_data_list.setMinimumHeight(120) self.add_data_list.setToolTip(_("Double-click to edit target path; Drag & drop supported")) - + self.add_data_list.itemDoubleClicked.connect(self.edit_resource) self.add_data_list.itemsDropped.connect(self.on_resources_dropped) c_lay_res.addWidget(self.add_data_list) - + btn_res_lay = QHBoxLayout() self.btn_add_file = QPushButton(_("Add File")) self.btn_add_file.setProperty("class", "ToolBtn") self.btn_add_file.clicked.connect(self.add_resource_files) - + self.btn_add_dir = QPushButton(_("Add Dir")) self.btn_add_dir.setProperty("class", "ToolBtn") self.btn_add_dir.clicked.connect(self.add_resource_dir) - + self.btn_del_res = QPushButton(_("Remove Selected")) self.btn_del_res.setProperty("class", "ToolBtn") self.btn_del_res.clicked.connect(self.del_resource) - + self.btn_clear_res = QPushButton(_("Clear All")) self.btn_clear_res.setProperty("class", "ToolBtn") self.btn_clear_res.clicked.connect(self.clear_resource) - + btn_res_lay.addWidget(self.btn_add_file) btn_res_lay.addWidget(self.btn_add_dir) btn_res_lay.addWidget(self.btn_del_res) @@ -2770,35 +3265,35 @@ class SettingsPanel(QWidget): self.form_opt = QFormLayout() self.form_opt.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) self.form_opt.setSpacing(15) - + self.cores_spin = QSpinBox() self.cores_spin.setRange(1, os.cpu_count() or 4) self.cores_spin.setValue(os.cpu_count() or 2) - + self.lbl_cpu_cores = QLabel(_("CPU Cores:")) self.form_opt.addRow(self.lbl_cpu_cores, self.cores_spin) - + self.upx_check = QCheckBox(_("Enable UPX Compression")) self.upx_check.toggled.connect(self.on_upx_toggled) - + self.upx_path_edit = QLineEdit() self.upx_path_edit.setPlaceholderText(_("Leave blank to auto-detect from environment variables")) - + self.btn_upx = QPushButton(_("Browse")) self.btn_upx.setProperty("class", "ToolBtn") self.btn_upx.clicked.connect(self.select_upx_path) - + self.upx_path_container = QWidget() h_upx = QHBoxLayout(self.upx_path_container) - h_upx.setContentsMargins(0,0,0,0) + h_upx.setContentsMargins(0, 0, 0, 0) h_upx.addWidget(self.upx_path_edit, 1) h_upx.addWidget(self.btn_upx) self.upx_path_container.setVisible(False) - + h_upx_row = QHBoxLayout() h_upx_row.addWidget(self.upx_check) h_upx_row.addWidget(self.upx_path_container) - + self.lbl_upx_path = QLabel(_("UPX Path:")) self.form_opt.addRow(self.lbl_upx_path, h_upx_row) c_lay_opt.addLayout(self.form_opt) @@ -2815,7 +3310,7 @@ class SettingsPanel(QWidget): self.cert_path_edit = QLineEdit() self.cert_path_edit.setPlaceholderText(_("Select .pfx / .p12 certificate file")) - + self.btn_cert = QPushButton(_("Browse")) self.btn_cert.setProperty("class", "ToolBtn") self.btn_cert.clicked.connect(self.select_cert_file) @@ -2843,9 +3338,11 @@ class SettingsPanel(QWidget): form_adv_sign.addRow(self.lbl_cert_pass, self.cert_pass_edit) self.adv_sign_container.setVisible(False) - self.btn_toggle_adv_sign = QPushButton(_("Use Custom Commercial Cert (Optional) ▸")) + self.btn_toggle_adv_sign = QPushButton(_("Use Custom Commercial Cert (Optional) ")) self.btn_toggle_adv_sign.setFlat(True) - self.btn_toggle_adv_sign.setStyleSheet("QPushButton { color: #2563eb; font-size: 12px; text-align: left; background: transparent; border: none; font-weight: bold; } QPushButton:hover { text-decoration: underline; }") + self.btn_toggle_adv_sign.setStyleSheet( + "QPushButton { color: #2563eb; font-size: 12px; text-align: left; background: transparent; border: none; font-weight: bold; } QPushButton:hover { text-decoration: underline; }" + ) self.btn_toggle_adv_sign.clicked.connect(self.toggle_adv_sign_ui) c_lay_sign.addWidget(self.btn_toggle_adv_sign) @@ -2855,13 +3352,13 @@ class SettingsPanel(QWidget): self.form_ver = QFormLayout() self.form_ver.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) self.form_ver.setSpacing(15) - + self.pyi_ver_edit = QLineEdit() self.nuitka_ver_edit = QLineEdit() - + self.lbl_pyi_ver = QLabel(_("PyInstaller Version:")) self.lbl_nuitka_ver = QLabel(_("Nuitka Version:")) - + self.form_ver.addRow(self.lbl_pyi_ver, self.pyi_ver_edit) self.form_ver.addRow(self.lbl_nuitka_ver, self.nuitka_ver_edit) c_lay_ver.addLayout(self.form_ver) @@ -2886,15 +3383,15 @@ class SettingsPanel(QWidget): self.btn_add_map = QPushButton(_("Add Mapping")) self.btn_add_map.setProperty("class", "ToolBtn") self.btn_add_map.clicked.connect(self.add_mapping_item) - + self.btn_del_map = QPushButton(_("Remove Selected")) self.btn_del_map.setProperty("class", "ToolBtn") self.btn_del_map.clicked.connect(self.delete_mapping_item) - + self.btn_reset_map = QPushButton(_("Restore Defaults")) self.btn_reset_map.setProperty("class", "ToolBtn") self.btn_reset_map.clicked.connect(self.reset_mapping_default) - + btn_map_lay.addWidget(self.btn_add_map) btn_map_lay.addWidget(self.btn_del_map) btn_map_lay.addWidget(self.btn_reset_map) @@ -2902,7 +3399,7 @@ class SettingsPanel(QWidget): c_lay_map.addLayout(btn_map_lay) self.card_rules, c_lay_rules = self._create_card(_("Obsolete Backport Exclusion Rules")) - + self.enable_shield_check = QCheckBox(_("Enable Automatic Backport Isolation")) self.enable_shield_check.setChecked(True) c_lay_rules.addWidget(self.enable_shield_check) @@ -2947,12 +3444,12 @@ class SettingsPanel(QWidget): self.card_lang, c_lay_lang = self._create_card(_("UI Language:")) form_lang = QFormLayout() self.lang_combo = QComboBox() - + for code, name in I18N.get_available_languages().items(): self.lang_combo.addItem(name, code) - + setup_combo_white_theme(self.lang_combo) - + self.lbl_lang_title = QLabel(_("UI Language:")) form_lang.addRow(self.lbl_lang_title, self.lang_combo) c_lay_lang.addLayout(form_lang) @@ -2961,36 +3458,36 @@ class SettingsPanel(QWidget): self.form_meta = QFormLayout() self.form_meta.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) self.form_meta.setSpacing(15) - + self.ver_ver = QLineEdit("1.0.0") self.ver_comp = QLineEdit(_("Independent Developer")) self.ver_desc = QLineEdit(_("Desktop Application")) - + self.lbl_ver_title = QLabel(_("Version:")) self.lbl_company_title = QLabel(_("Author/Company:")) self.lbl_desc_title = QLabel(_("Description:")) - + self.form_meta.addRow(self.lbl_ver_title, self.ver_ver) self.form_meta.addRow(self.lbl_company_title, self.ver_comp) self.form_meta.addRow(self.lbl_desc_title, self.ver_desc) c_lay_meta.addLayout(self.form_meta) c_lay_meta.addSpacing(10) - + g_preset = QGridLayout() g_preset.setSpacing(8) self.btn_exp_preset = QPushButton(_("Export Preset...")) self.btn_exp_preset.setProperty("class", "ToolBtn") self.btn_exp_preset.clicked.connect(self.export_preset) - + self.btn_imp_preset = QPushButton(_("Import Preset...")) self.btn_imp_preset.setProperty("class", "ToolBtn") self.btn_imp_preset.clicked.connect(self.import_preset) - + self.btn_reset_config = QPushButton(_("Reset to Default Config")) self.btn_reset_config.setProperty("class", "ToolBtn") self.btn_reset_config.clicked.connect(self.reset_global_config) - + g_preset.addWidget(self.btn_exp_preset, 0, 0) g_preset.addWidget(self.btn_imp_preset, 0, 1) g_preset.addWidget(self.btn_reset_config, 1, 0, 1, 2) @@ -3001,34 +3498,31 @@ class SettingsPanel(QWidget): self.out_mode_combo.addItems([_("Source File Directory"), _("Custom Directory")]) setup_combo_white_theme(self.out_mode_combo) self.out_mode_combo.currentIndexChanged.connect(self.on_out_mode_changed) - + self.sandbox_mode_combo = QComboBox() - self.sandbox_mode_combo.addItems([ - _("Source Directory (.qpypack_build)"), - _("System Temp Directory") - ]) + self.sandbox_mode_combo.addItems([_("Source Directory (.qpypack_build)"), _("System Temp Directory")]) setup_combo_white_theme(self.sandbox_mode_combo) self.out_dir_edit = QLineEdit() self.btn_out_dir = QPushButton(_("Browse")) self.btn_out_dir.setProperty("class", "ToolBtn") self.btn_out_dir.clicked.connect(self.select_out_dir) - + self.out_dir_container = QWidget() h_out_dir = QHBoxLayout(self.out_dir_container) h_out_dir.setContentsMargins(0, 0, 0, 0) h_out_dir.addWidget(self.out_dir_edit, 1) h_out_dir.addWidget(self.btn_out_dir) self.out_dir_container.setVisible(False) - + self.form_out = QFormLayout() self.form_out.setVerticalSpacing(15) self.form_out.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) - + self.lbl_out_rule_title = QLabel(_("Output Location:")) self.lbl_target_out_title = QLabel(_("Target Directory:")) self.lbl_sandbox_title = QLabel(_("Temporary Directory:")) - + self.form_out.addRow(self.lbl_out_rule_title, self.out_mode_combo) self.form_out.addRow(self.lbl_target_out_title, self.out_dir_container) self.form_out.addRow(self.lbl_sandbox_title, self.sandbox_mode_combo) @@ -3036,16 +3530,16 @@ class SettingsPanel(QWidget): self.card2, lay2 = self._create_card(_("Preferences & System Behavior")) lay2.setSpacing(16) - + self.concise_log_check = QCheckBox(_("Concise Log Output")) self.auto_save_log_check = QCheckBox(_("Auto-save Build Log")) self.auto_icon_check = QCheckBox(_("Auto Extract Icon")) self.clean_all_check = QCheckBox(_("Clean Temporary Cache After Build")) self.sound_notify_check = QCheckBox(_("Sound Notification")) - + for chk in (self.concise_log_check, self.auto_save_log_check, self.auto_icon_check, self.clean_all_check, self.sound_notify_check): lay2.addWidget(chk) - + self.lay_pref.addWidget(self.card_lang) self.lay_pref.addWidget(self.card_meta) self.lay_pref.addWidget(self.card1) @@ -3057,7 +3551,7 @@ class SettingsPanel(QWidget): main_lay.setContentsMargins(40, 10, 40, 10) main_lay.setSpacing(15) main_lay.addStretch(1) - + logo_lbl = QLabel() icon_path = get_resource_path("icon.ico") if os.path.exists(icon_path): @@ -3066,50 +3560,50 @@ class SettingsPanel(QWidget): logo_pixmap = high_res_pixmap.scaled(96, 96, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) logo_lbl.setPixmap(logo_pixmap) else: - logo_lbl.setPixmap(get_svg_pixmap('package', color="#1A73E8", size=96)) + logo_lbl.setPixmap(get_svg_pixmap("package", color="#1A73E8", size=96)) else: - logo_lbl.setPixmap(get_svg_pixmap('package', color="#1A73E8", size=96)) - + logo_lbl.setPixmap(get_svg_pixmap("package", color="#1A73E8", size=96)) + logo_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) main_lay.addWidget(logo_lbl) - + text_vlay = QVBoxLayout() text_vlay.setSpacing(6) text_vlay.setAlignment(Qt.AlignmentFlag.AlignCenter) - + title_lbl = QLabel(__app_name__) title_lbl.setStyleSheet("font-size: 32px; font-weight: 900; color: #202124; letter-spacing: -1px;") title_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) text_vlay.addWidget(title_lbl) - + self.about_desc_lbl = QLabel(_("Modern Cross-Platform Python Packaging GUI Powered by PyInstaller & Nuitka")) self.about_desc_lbl.setStyleSheet("font-size: 14px; color: #3c4043;") self.about_desc_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) text_vlay.addWidget(self.about_desc_lbl) - + text_vlay.addSpacing(6) - + current_year = time.localtime().tm_year year_str = f"2026-{current_year}" if current_year > 2026 else "2026" ver_lbl = QLabel(f"Version {__version__} · GPL-3.0 · Copyright © {year_str} {__author__}") ver_lbl.setStyleSheet("font-size: 12px; color: #8b929a; font-weight: bold;") ver_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) text_vlay.addWidget(ver_lbl) - + main_lay.addLayout(text_vlay) main_lay.addSpacing(25) btn_lay = QHBoxLayout() btn_lay.setSpacing(12) btn_lay.setAlignment(Qt.AlignmentFlag.AlignCenter) - + def create_link_btn(text, url, svg_path, icon_color, bg_color, hover_bg, pressed_bg): btn = QPushButton(" " + text) btn.setCursor(Qt.CursorShape.PointingHandCursor) - + svg_str = f'' renderer = QSvgRenderer() - renderer.load(svg_str.encode('utf-8')) + renderer.load(svg_str.encode("utf-8")) pixmap = QPixmap(48, 48) pixmap.fill(Qt.GlobalColor.transparent) painter = QPainter(pixmap) @@ -3118,90 +3612,107 @@ class SettingsPanel(QWidget): renderer.render(painter) painter.end() btn.setIcon(QIcon(pixmap)) - + btn.setStyleSheet(f""" QPushButton {{ background-color: {bg_color}; color: #3c4043; border: none; border-radius: 8px; padding: 8px 16px; font-size: 13px; font-weight: bold; }} QPushButton:hover {{ background-color: {hover_bg}; color: {icon_color}; }} QPushButton:pressed {{ background-color: {pressed_bg}; }} """) - btn.clicked.connect(lambda: __import__('webbrowser').open(url)) + btn.clicked.connect(lambda: __import__("webbrowser").open(url)) return btn - + p_github = "M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.603-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.462-1.11-1.462-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.831.092-.646.35-1.086.636-1.336-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0112 6.836c.85.004 1.705.114 2.504.336 1.909-1.294 2.747-1.025 2.747-1.025.546 1.379.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.161 22 16.416 22 12c0-5.523-4.477-10-10-10z" p_issue = "M20 8h-2.81c-.45-.78-1.07-1.45-1.82-1.96L17 4.41 15.59 3l-2.17 2.17C12.96 5.06 12.49 5 12 5c-.49 0-.96.06-1.41.17L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6 8h-4v-2h4v2zm0-4h-4v-2h4v2z" p_pypi = "M12.06,1.48c-3.14,0-3.52,0.67-3.52,0.67l-0.01,2.44h3.63v0.52H7.43C5.12,5.11,4.5,6.58,4.5,8.81c0,2.34,0.38,3.48,2.3,3.48 h1.14v-1.62c0-1.48,1.23-2.65,2.7-2.65h3.69c1.47,0,2.66-1.19,2.66-2.65V3.88C16.99,1.83,14.67,1.48,12.06,1.48z M10.22,2.83 c0.41,0,0.73,0.33,0.73,0.74c0,0.41-0.33,0.74-0.73,0.74C9.49,3.16,9.82,2.83,10.22,2.83z M16.71,9.89 v1.62c0,1.48-1.23,2.65-2.7,2.65H10.3c-1.47,0-2.66,1.19-2.66,2.65v1.49c0,2.05,2.32,2.41,4.92,2.41c3.14,0,3.52-0.67,3.52-0.67 l0.01-2.44h-3.63v-0.52h4.73c2.31,0,2.93-1.47,2.93-3.7c0-2.34-0.38-3.48-2.3-3.48H16.71z M13.88,18.96c0.41,0,0.73,0.33,0.73,0.74c0,0.41-0.33,0.74-0.73,0.74c-0.4,0-0.73-0.33-0.73-0.74C13.15,19.29,13.48,18.96,13.88,18.96z" p_heart = "M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" - self.btn_github = create_link_btn(_("GitHub Repository"), "https://github.com/qwejay/QPyPack", p_github, "#24292e", "#f1f3f4", "#e8eaed", "#dadce0") - self.btn_issue = create_link_btn(_("Issues & Feedback"), "https://github.com/qwejay/QPyPack/issues", p_issue, "#d93025", "#f1f3f4", "#e8eaed", "#dadce0") - self.btn_pypi = create_link_btn(_("PyPI Home"), "https://pypi.org/project/qpypack/", p_pypi, "#1A73E8", "#f1f3f4", "#e8eaed", "#dadce0") - - self.btn_sponsor = create_link_btn(_("Sponsor"), "https://www.ifdian.net/a/qwejay", p_heart, "#d93025", "#fce8e6", "#fad2cf", "#f6aea9") + self.btn_github = create_link_btn( + _("GitHub Repository"), "https://github.com/qwejay/QPyPack", p_github, "#24292e", "#f1f3f4", "#e8eaed", "#dadce0" + ) + self.btn_issue = create_link_btn( + _("Issues & Feedback"), "https://github.com/qwejay/QPyPack/issues", p_issue, "#d93025", "#f1f3f4", "#e8eaed", "#dadce0" + ) + self.btn_pypi = create_link_btn( + _("PyPI Home"), "https://pypi.org/project/qpypack/", p_pypi, "#1A73E8", "#f1f3f4", "#e8eaed", "#dadce0" + ) + + self.btn_sponsor = create_link_btn( + _("Sponsor"), "https://www.ifdian.net/a/qwejay", p_heart, "#d93025", "#fce8e6", "#fad2cf", "#f6aea9" + ) self.btn_sponsor.setStyleSheet(self.btn_sponsor.styleSheet().replace("color: #3c4043;", "color: #d93025;")) - + btn_lay.addWidget(self.btn_github) btn_lay.addWidget(self.btn_issue) btn_lay.addWidget(self.btn_pypi) btn_lay.addWidget(self.btn_sponsor) - + main_lay.addLayout(btn_lay) main_lay.addSpacing(20) - + sponsor_vlay = QVBoxLayout() sponsor_vlay.setSpacing(12) - - self.sponsor_desc_p1 = QLabel(_("QPyPack is a free and open-source tool. If it has improved your efficiency or solved packaging problems, consider buying the author a coffee!")) + + self.sponsor_desc_p1 = QLabel( + _( + "QPyPack is a free and open-source tool. If it has improved your efficiency or solved packaging problems, consider buying the author a coffee!" + ) + ) self.sponsor_desc_p1.setWordWrap(True) self.sponsor_desc_p1.setStyleSheet("font-size: 12px; color: #5f6368;") self.sponsor_desc_p1.setAlignment(Qt.AlignmentFlag.AlignCenter) sponsor_vlay.addWidget(self.sponsor_desc_p1) - - self.sponsor_desc_p2 = QLabel(_("* Sponsorship is completely voluntary, serves as an unconditional encouragement to the open-source community, and involves no commercial commitments. Thank you for your support!")) + + self.sponsor_desc_p2 = QLabel( + _( + "* Sponsorship is completely voluntary, serves as an unconditional encouragement to the open-source community, and involves no commercial commitments. Thank you for your support!" + ) + ) self.sponsor_desc_p2.setWordWrap(True) self.sponsor_desc_p2.setStyleSheet("font-size: 11px; color: #9aa0a6;") self.sponsor_desc_p2.setAlignment(Qt.AlignmentFlag.AlignCenter) sponsor_vlay.addWidget(self.sponsor_desc_p2) - + main_lay.addLayout(sponsor_vlay) main_lay.addStretch(1) - def retranslate_ui(self): self.tabs.setTabText(0, _("Build Settings")) self.tabs.setTabText(1, _("Preferences")) self.tabs.setTabText(2, _("About")) - + self.sub_tabs.setTabText(0, _("Engine")) self.sub_tabs.setTabText(1, _("Dependencies")) self.sub_tabs.setTabText(2, _("Resources")) self.sub_tabs.setTabText(3, _("Optimization & Security")) self.sub_tabs.setTabText(4, _("Package Mappings & Rules")) - + self.card_engine.findChild(QLabel, "CardTitle").setText(_("Engine & Environment")) self.card_mode.findChild(QLabel, "CardTitle").setText(_("Execution Mode")) self.card_deps.findChild(QLabel, "CardTitle").setText(_("Mirrors & Scanner")) self.card_res.findChild(QLabel, "CardTitle").setText(_("Additional Resources (Drag & Drop Supported)")) self.card_opt.findChild(QLabel, "CardTitle").setText(_("Performance Optimization")) + if hasattr(self, "card_sign"): + self.card_sign.findChild(QLabel, "CardTitle").setText(_("Code Signing")) self.card_ver.findChild(QLabel, "CardTitle").setText(_("Lock Core Dependencies")) self.card_map.findChild(QLabel, "CardTitle").setText(_("Package Name Mappings")) - if hasattr(self, 'card_rules'): + if hasattr(self, "card_rules"): self.card_rules.findChild(QLabel, "CardTitle").setText(_("Obsolete Backport Exclusion Rules")) - if hasattr(self, 'card_env'): + if hasattr(self, "card_env"): self.card_env.findChild(QLabel, "CardTitle").setText(_("Environment Analysis")) - if hasattr(self, 'enable_shield_check'): + if hasattr(self, "enable_shield_check"): self.enable_shield_check.setText(_("Enable Automatic Backport Isolation")) - if hasattr(self, 'btn_inspect_env'): + if hasattr(self, "btn_inspect_env"): self.btn_inspect_env.setText(_("Inspect Interpreter Environment")) - if hasattr(self, 'btn_remediate_env'): + if hasattr(self, "btn_remediate_env"): self.btn_remediate_env.setText(_("Remediate Detected Conflicts")) - if hasattr(self, 'backport_table'): + if hasattr(self, "backport_table"): self.backport_table.setHorizontalHeaderLabels([_("Package Name"), _("Min Python Ver")]) - if hasattr(self, 'btn_add_rule'): + if hasattr(self, "btn_add_rule"): self.btn_add_rule.setText(_("Add Backport Rule")) - if hasattr(self, 'btn_del_rule'): + if hasattr(self, "btn_del_rule"): self.btn_del_rule.setText(_("Remove Selected")) - if hasattr(self, 'btn_reset_rules'): + if hasattr(self, "btn_reset_rules"): self.btn_reset_rules.setText(_("Restore Defaults")) self.card_lang.findChild(QLabel, "CardTitle").setText(_("UI Language:")) @@ -3244,7 +3755,7 @@ class SettingsPanel(QWidget): if not self.ver_desc.isModified(): self.ver_desc.setText(_("Desktop Application")) - if hasattr(self, 'pip_source_combo') and hasattr(self, 'pip_backup_combo'): + if hasattr(self, "pip_source_combo") and hasattr(self, "pip_backup_combo"): cur_main = self._get_url_from_combo(self.pip_source_combo) cur_back = self._get_url_from_combo(self.pip_backup_combo) @@ -3288,7 +3799,7 @@ class SettingsPanel(QWidget): self.btn_exp_preset.setText(_("Export Preset...")) self.btn_imp_preset.setText(_("Import Preset...")) self.btn_reset_config.setText(_("Reset to Default Config")) - + self.python_path_combo.setPlaceholderText(_("Leave blank to auto-detect system default Python")) self.name_edit.setPlaceholderText(_("Leave blank to auto-match script name")) self.reqs_file_edit.setPlaceholderText(_("Leave blank to auto-search requirements.txt in current directory")) @@ -3307,13 +3818,13 @@ class SettingsPanel(QWidget): self.pipreqs_check.setText(_("Analyze Dependencies (AST)")) self.pipreqs_dir_check.setText(_("Scan Entire Folder")) self.upx_check.setText(_("Enable UPX Compression")) - + self.concise_log_check.setText(_("Concise Log Output")) self.auto_save_log_check.setText(_("Auto-save Build Log")) self.auto_icon_check.setText(_("Auto Extract Icon")) self.clean_all_check.setText(_("Clean Temporary Cache After Build")) self.sound_notify_check.setText(_("Sound Notification")) - + self.out_mode_combo.setItemText(0, _("Source File Directory")) self.out_mode_combo.setItemText(1, _("Custom Directory")) self.mapping_table.setHorizontalHeaderLabels([_("Import Name"), _("PyPI Package Name")]) @@ -3321,31 +3832,39 @@ class SettingsPanel(QWidget): self.rb_venv_shared.setText(_("Shared Environment")) self.shared_venv_dir_edit.setPlaceholderText(_("Select shared directory...")) - if hasattr(self, 'sponsor_desc_p1'): - self.sponsor_desc_p1.setText(_("QPyPack is a free and open-source tool. If it has improved your efficiency or solved packaging problems, consider buying the author a coffee!")) - self.sponsor_desc_p2.setText(_("* Sponsorship is completely voluntary, serves as an unconditional encouragement to the open-source community, and involves no commercial commitments. Thank you for your support!")) + if hasattr(self, "sponsor_desc_p1"): + self.sponsor_desc_p1.setText( + _( + "QPyPack is a free and open-source tool. If it has improved your efficiency or solved packaging problems, consider buying the author a coffee!" + ) + ) + self.sponsor_desc_p2.setText( + _( + "* Sponsorship is completely voluntary, serves as an unconditional encouragement to the open-source community, and involves no commercial commitments. Thank you for your support!" + ) + ) self.btn_reset.setToolTip(_("Reset to Default Config")) self.btn_back.setToolTip(_("Cancel & Return")) - if hasattr(self, 'lbl_res_hint'): + if hasattr(self, "lbl_res_hint"): self.lbl_res_hint.setText(_("Project-specific. Not saved to global preferences. Use 'Export Preset' to save config.")) self.add_data_list.setToolTip(_("Double-click to edit target path; Drag & drop supported. Use 'Export Preset' to save for reuse.")) - + self.btn_clean_venvs.setText(_("Clear Local Venvs")) self.enable_sign_check.setText(_("Enable Smart Code Signing")) self.enable_sign_check.setToolTip(_("Auto-applies digital signature to built app, eliminating 'Unknown Publisher' warning")) - - if hasattr(self, 'lbl_cert_path'): + + if hasattr(self, "lbl_cert_path"): self.lbl_cert_path.setText(_("Commercial PFX File:")) - if hasattr(self, 'lbl_cert_pass'): + if hasattr(self, "lbl_cert_pass"): self.lbl_cert_pass.setText(_("Cert Password:")) if self.adv_sign_container.isVisible(): - self.btn_toggle_adv_sign.setText(_("Hide Custom Cert Settings ▾")) + self.btn_toggle_adv_sign.setText(_("Hide Custom Cert Settings ")) else: - self.btn_toggle_adv_sign.setText(_("Use Custom Commercial Cert (Optional) ▸")) + self.btn_toggle_adv_sign.setText(_("Use Custom Commercial Cert (Optional) ")) - if hasattr(self, 'lang_combo'): + if hasattr(self, "lang_combo"): current_lang = self.lang_combo.currentData() self.lang_combo.blockSignals(True) self.lang_combo.clear() @@ -3356,10 +3875,10 @@ class SettingsPanel(QWidget): self.lang_combo.setCurrentIndex(idx) self.lang_combo.blockSignals(False) - if hasattr(self, 'lbl_env_status'): + if hasattr(self, "lbl_env_status"): if "ready." in self.lbl_env_status.text() or "等待环境校验" in self.lbl_env_status.text(): self.lbl_env_status.setText(_("Status: Environment inspection ready.")) - + self.on_engine_changed() self.on_python_path_changed() @@ -3368,14 +3887,16 @@ class SettingsPanel(QWidget): self.python_path_combo.clear() for path, ver in py_dict.items(): self.python_path_combo.addItem(f"{path} (Python {ver})", path) - - if not current_text: current_text = get_python_executable() - + + if not current_text: + current_text = get_python_executable() + if current_text: clean_text = current_text - if " (Python " in clean_text: clean_text = clean_text.split(" (Python ")[0].strip() + if " (Python " in clean_text: + clean_text = clean_text.split(" (Python ")[0].strip() clean_text = os.path.normpath(clean_text).lower() - + found = False for idx in range(self.python_path_combo.count()): item_path = self.python_path_combo.itemData(idx) @@ -3383,15 +3904,15 @@ class SettingsPanel(QWidget): self.python_path_combo.setCurrentIndex(idx) found = True break - if not found: self.python_path_combo.setCurrentText(current_text) + if not found: + self.python_path_combo.setCurrentText(current_text) fm = self.python_path_combo.fontMetrics() max_width = 520 for i in range(self.python_path_combo.count()): text_w = fm.horizontalAdvance(self.python_path_combo.itemText(i)) - if text_w > max_width: - max_width = text_w - + max_width = max(max_width, text_w) + if self.python_path_combo.view(): self.python_path_combo.view().setMinimumWidth(max_width + 36) @@ -3407,23 +3928,28 @@ class SettingsPanel(QWidget): def add_mapping_item(self): dlg1 = CustomInputDialog(self, _("Add Mapping"), _("Import name (e.g. cv2):")) - if dlg1.exec() != QDialog.DialogCode.Accepted: return + if dlg1.exec() != QDialog.DialogCode.Accepted: + return imp_name = dlg1.get_text().strip() - if not imp_name: return - + if not imp_name: + return + dlg2 = CustomInputDialog(self, _("Add Mapping"), _("PyPI package name for [{imp_name}]:", imp_name=imp_name), imp_name) - if dlg2.exec() != QDialog.DialogCode.Accepted: return + if dlg2.exec() != QDialog.DialogCode.Accepted: + return pypi_name = dlg2.get_text().strip() - if not pypi_name: return - + if not pypi_name: + return + row = self.mapping_table.rowCount() self.mapping_table.insertRow(row) self.mapping_table.setItem(row, 0, QTableWidgetItem(imp_name)) self.mapping_table.setItem(row, 1, QTableWidgetItem(pypi_name)) def delete_mapping_item(self): - rows = set(item.row() for item in self.mapping_table.selectedItems()) - for r in sorted(rows, reverse=True): self.mapping_table.removeRow(r) + rows = {item.row() for item in self.mapping_table.selectedItems()} + for r in sorted(rows, reverse=True): + self.mapping_table.removeRow(r) def reset_mapping_default(self): self.populate_mapping_table(DEFAULT_MAPPINGS) @@ -3440,23 +3966,28 @@ class SettingsPanel(QWidget): def add_backport_rule_item(self): dlg1 = CustomInputDialog(self, _("Add Backport Rule"), _("Package Name (e.g. pathlib):")) - if dlg1.exec() != QDialog.DialogCode.Accepted: return + if dlg1.exec() != QDialog.DialogCode.Accepted: + return pkg_name = dlg1.get_text().strip().lower() - if not pkg_name: return - + if not pkg_name: + return + dlg2 = CustomInputDialog(self, _("Add Backport Rule"), _("Minimum Built-in Python Version (e.g. 3.4):"), "3.4") - if dlg2.exec() != QDialog.DialogCode.Accepted: return + if dlg2.exec() != QDialog.DialogCode.Accepted: + return ver_str = dlg2.get_text().strip() - if not ver_str: ver_str = "3.4" - + if not ver_str: + ver_str = "3.4" + row = self.backport_table.rowCount() self.backport_table.insertRow(row) self.backport_table.setItem(row, 0, QTableWidgetItem(pkg_name)) self.backport_table.setItem(row, 1, QTableWidgetItem(ver_str)) def delete_backport_rule_item(self): - rows = set(item.row() for item in self.backport_table.selectedItems()) - for r in sorted(rows, reverse=True): self.backport_table.removeRow(r) + rows = {item.row() for item in self.backport_table.selectedItems()} + for r in sorted(rows, reverse=True): + self.backport_table.removeRow(r) def reset_backport_rules_default(self): self.populate_backport_table(DEFAULT_BACKPORT_RULES) @@ -3465,20 +3996,24 @@ class SettingsPanel(QWidget): def get_selected_python_ver_tuple(self): raw_py = self.python_path_combo.currentText().strip() - if " (Python " in raw_py: raw_py = raw_py.split(" (Python ")[0].strip() - if not raw_py: raw_py = get_python_executable() - + if " (Python " in raw_py: + raw_py = raw_py.split(" (Python ")[0].strip() + if not raw_py: + raw_py = get_python_executable() + try: clean_env = os.environ.copy() clean_env.pop("PYTHONHOME", None) clean_env.pop("PYTHONPATH", None) kw = {"capture_output": True, "text": True, "timeout": 3, "errors": "ignore"} - if os.name == 'nt': kw["creationflags"] = subprocess.CREATE_NO_WINDOW + if os.name == "nt": + kw["creationflags"] = subprocess.CREATE_NO_WINDOW res = subprocess.run([raw_py, "-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"], **kw) if res.returncode == 0 and res.stdout.strip(): - parts = res.stdout.strip().split('.') + parts = res.stdout.strip().split(".") return raw_py, (int(parts[0]), int(parts[1])) - except Exception: pass + except Exception: + pass return raw_py, (3, 8) def get_current_active_backport_rules(self): @@ -3490,7 +4025,7 @@ class SettingsPanel(QWidget): ver_str = item_v.text().strip() if item_v else "3.4" if pkg: try: - parts = [int(x) for x in ver_str.split('.')] + parts = [int(x) for x in ver_str.split(".")] major = parts[0] if len(parts) > 0 else 3 minor = parts[1] if len(parts) > 1 else 0 rules[pkg] = (major, minor) @@ -3503,18 +4038,18 @@ class SettingsPanel(QWidget): if not py_exe or not os.path.exists(py_exe): self.lbl_env_status.setText(_("[ERROR] Python interpreter is invalid or not found: {path}", path=py_exe or "None")) return - + installed = query_target_env_packages(py_exe) current_rules = self.get_current_active_backport_rules() conflicts_found = [] - - for pkg_name in installed.keys(): + + for pkg_name in installed: clean_pkg = pkg_name.lower().strip() if clean_pkg in current_rules: req_ver_tuple = current_rules[clean_pkg] if ver_tuple >= req_ver_tuple: conflicts_found.append(pkg_name) - + if conflicts_found: msg = _("Active backport conflicts detected ({count}): {pkgs}", count=len(conflicts_found), pkgs=", ".join(conflicts_found)) self.lbl_env_status.setText(f"[WARN] {msg}") @@ -3525,22 +4060,23 @@ class SettingsPanel(QWidget): py_exe, ver_tuple = self.get_selected_python_ver_tuple() installed = query_target_env_packages(py_exe) current_rules = self.get_current_active_backport_rules() - + conflicts_to_remove = [] - for pkg_name in installed.keys(): + for pkg_name in installed: clean_pkg = pkg_name.lower().strip() if clean_pkg in current_rules and ver_tuple >= current_rules[clean_pkg]: conflicts_to_remove.append(pkg_name) - + if not conflicts_to_remove: if hasattr(self.parent_win, "show_notification"): self.parent_win.show_notification(_("No environment conflicts detected.")) return - + uninst_cmd = [py_exe, "-m", "pip", "uninstall", "-y"] + conflicts_to_remove try: kw = {"capture_output": True, "text": True, "timeout": 20, "errors": "ignore"} - if os.name == 'nt': kw["creationflags"] = subprocess.CREATE_NO_WINDOW + if os.name == "nt": + kw["creationflags"] = subprocess.CREATE_NO_WINDOW subprocess.run(uninst_cmd, **kw) self.inspect_current_python_env() if hasattr(self.parent_win, "show_notification"): @@ -3552,32 +4088,39 @@ class SettingsPanel(QWidget): is_visible = not self.adv_sign_container.isVisible() self.adv_sign_container.setVisible(is_visible) if is_visible: - self.btn_toggle_adv_sign.setText(_("Hide Custom Cert Settings ▾")) + self.btn_toggle_adv_sign.setText(_("Hide Custom Cert Settings ")) else: - self.btn_toggle_adv_sign.setText(_("Use Custom Commercial Cert (Optional) ▸")) + self.btn_toggle_adv_sign.setText(_("Use Custom Commercial Cert (Optional) ")) def select_cert_file(self): - f, _filter = QFileDialog.getOpenFileName(self, _("Commercial PFX File:"), "", "PKCS#12 Certificate (*.pfx *.p12);;All Files (*)", options=QFileDialog.Option.DontUseNativeDialog) - if f: self.cert_path_edit.setText(Path(f).resolve().as_posix()) + f, _filter = QFileDialog.getOpenFileName( + self, + _("Commercial PFX File:"), + "", + "PKCS#12 Certificate (*.pfx *.p12);;All Files (*)", + options=QFileDialog.Option.DontUseNativeDialog, + ) + if f: + self.cert_path_edit.setText(Path(f).resolve().as_posix()) def create_self_signed_cert(self): - if os.name != 'nt': + if os.name != "nt": if hasattr(self.parent_win, "show_notification"): self.parent_win.show_notification(_("Only supported on Windows.")) return - + dlg = GenCertDialog(self) if dlg.exec() == QDialog.DialogCode.Accepted: subject, pwd = dlg.cert_info - + fp, _filter = QFileDialog.getSaveFileName( - self, - _("Save Certificate"), - "my_cert.pfx", + self, + _("Save Certificate"), + "my_cert.pfx", _("Self-Signed Certificate (*.pfx)"), - options=QFileDialog.Option.DontUseNativeDialog + options=QFileDialog.Option.DontUseNativeDialog, ) - + if fp: try: fp_posix = Path(fp).resolve().as_posix() @@ -3586,11 +4129,16 @@ class SettingsPanel(QWidget): ps_cmd = ( f'$subject = "CN={safe_subject}"; ' f'$cert = New-SelfSignedCertificate -Type CodeSigningCert -Subject $subject -CertStoreLocation "Cert:\\CurrentUser\\My"; ' - f'$pwd = ConvertTo-SecureString -String \'{safe_pwd}\' -Force -AsPlainText; ' + f"$pwd = ConvertTo-SecureString -String '{safe_pwd}' -Force -AsPlainText; " f'Export-PfxCertificate -Cert $cert -FilePath "{fp_posix}" -Password $pwd; ' f'Remove-Item -Path "Cert:\\CurrentUser\\My\\$($cert.Thumbprint)"' ) - res = subprocess.run(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", ps_cmd], capture_output=True, text=True, creationflags=subprocess.CREATE_NO_WINDOW) + res = subprocess.run( + ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", ps_cmd], + capture_output=True, + text=True, + creationflags=subprocess.CREATE_NO_WINDOW, + ) if res.returncode == 0 and Path(fp).exists(): self.cert_path_edit.setText(fp_posix) self.cert_pass_edit.setText(pwd) @@ -3601,10 +4149,16 @@ class SettingsPanel(QWidget): self.parent_win.show_error_log(_("Failed to generate certificate.") + f" {res.stderr}") except Exception as e: if hasattr(self.parent_win, "show_error_log"): - self.parent_win.show_error_log(_("Failed to generate certificate.") + f" {str(e)}") + self.parent_win.show_error_log(_("Failed to generate certificate.") + f" {e!s}") def export_preset(self): - fp, _filter = QFileDialog.getSaveFileName(self, _("Export Preset..."), "project_config.qpypack", "QPyPack Presets (*.qpypack *.json)", options=QFileDialog.Option.DontUseNativeDialog) + fp, _filter = QFileDialog.getSaveFileName( + self, + _("Export Preset..."), + "project_config.qpypack", + "QPyPack Presets (*.qpypack *.json)", + options=QFileDialog.Option.DontUseNativeDialog, + ) if fp: try: data = { @@ -3628,9 +4182,9 @@ class SettingsPanel(QWidget): "reqs_file": self.reqs_file_edit.text(), "pip_source": self._get_url_from_combo(self.pip_source_combo), "pip_backup": self._get_url_from_combo(self.pip_backup_combo), - "add_data_list": [self.add_data_list.item(i).data(Qt.ItemDataRole.UserRole) for i in range(self.add_data_list.count())] + "add_data_list": [self.add_data_list.item(i).data(Qt.ItemDataRole.UserRole) for i in range(self.add_data_list.count())], } - Path(fp).write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding='utf-8') + Path(fp).write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") if hasattr(self.parent_win, "show_notification"): self.parent_win.show_notification(_("Config preset exported to: {path}", path=fp)) except Exception as e: @@ -3638,11 +4192,18 @@ class SettingsPanel(QWidget): self.parent_win.show_error_log(_("[ERROR] Failed to export preset file: {error}", error=str(e))) def import_preset(self): - fp, _filter = QFileDialog.getOpenFileName(self, _("Import Preset..."), "", "QPyPack Presets (*.qpypack *.json);;All Files (*)", options=QFileDialog.Option.DontUseNativeDialog) + fp, _filter = QFileDialog.getOpenFileName( + self, + _("Import Preset..."), + "", + "QPyPack Presets (*.qpypack *.json);;All Files (*)", + options=QFileDialog.Option.DontUseNativeDialog, + ) if fp: try: - data = json.loads(Path(fp).read_text(encoding='utf-8')) - if "engine" in data: self.engine_combo.setCurrentText(data["engine"]) + data = json.loads(Path(fp).read_text(encoding="utf-8")) + if "engine" in data: + self.engine_combo.setCurrentText(data["engine"]) if "lite_mode" in data: self.rb_lite_mode.setChecked(bool(data["lite_mode"])) self.rb_compat_mode.setChecked(not bool(data["lite_mode"])) @@ -3654,24 +4215,39 @@ class SettingsPanel(QWidget): if "contents_dir" in data: self.contents_dir_edit.setText(data["contents_dir"]) self.on_mode_type_changed() - if "noconsole" in data: self.noconsole_check.setChecked(bool(data["noconsole"])) - if "icon" in data: self.icon_edit.setText(data["icon"]) - if "app_name" in data: self.name_edit.setText(data["app_name"]) - if "ver_ver" in data: self.ver_ver.setText(data["ver_ver"]) - if "ver_comp" in data: self.ver_comp.setText(data["ver_comp"]) - if "ver_desc" in data: self.ver_desc.setText(data["ver_desc"]) - if "hidden_imports" in data: self.hidden_edit.setText(data["hidden_imports"]) - if "exclude_modules" in data: self.exclude_edit.setText(data["exclude_modules"]) - if "use_venv" in data: + if "noconsole" in data: + self.noconsole_check.setChecked(bool(data["noconsole"])) + if "icon" in data: + self.icon_edit.setText(data["icon"]) + if "app_name" in data: + self.name_edit.setText(data["app_name"]) + if "ver_ver" in data: + self.ver_ver.setText(data["ver_ver"]) + if "ver_comp" in data: + self.ver_comp.setText(data["ver_comp"]) + if "ver_desc" in data: + self.ver_desc.setText(data["ver_desc"]) + if "hidden_imports" in data: + self.hidden_edit.setText(data["hidden_imports"]) + if "exclude_modules" in data: + self.exclude_edit.setText(data["exclude_modules"]) + if "use_venv" in data: self.venv_check.setChecked(bool(data["use_venv"])) self.keep_venv_check.setEnabled(bool(data["use_venv"])) - if "keep_venv" in data: self.keep_venv_check.setChecked(bool(data["keep_venv"])) - if "use_reqs" in data: self.reqs_check.setChecked(bool(data["use_reqs"])) - if "use_pipreqs" in data: self.pipreqs_check.setChecked(bool(data["use_pipreqs"])) - if "use_pipreqs_dir" in data: self.pipreqs_dir_check.setChecked(bool(data["use_pipreqs_dir"])) - if "reqs_file" in data: self.reqs_file_edit.setText(data["reqs_file"]) - if "pip_source" in data: self._set_combo_value(self.pip_source_combo, data["pip_source"]) - if "pip_backup" in data: self._set_combo_value(self.pip_backup_combo, data["pip_backup"]) + if "keep_venv" in data: + self.keep_venv_check.setChecked(bool(data["keep_venv"])) + if "use_reqs" in data: + self.reqs_check.setChecked(bool(data["use_reqs"])) + if "use_pipreqs" in data: + self.pipreqs_check.setChecked(bool(data["use_pipreqs"])) + if "use_pipreqs_dir" in data: + self.pipreqs_dir_check.setChecked(bool(data["use_pipreqs_dir"])) + if "reqs_file" in data: + self.reqs_file_edit.setText(data["reqs_file"]) + if "pip_source" in data: + self._set_combo_value(self.pip_source_combo, data["pip_source"]) + if "pip_backup" in data: + self._set_combo_value(self.pip_backup_combo, data["pip_backup"]) if "add_data_list" in data: self.add_data_list.clear() for item in data["add_data_list"]: @@ -3685,160 +4261,166 @@ class SettingsPanel(QWidget): def load_from_config(self): config = load_config() - if 'Settings' in config: - s = config['Settings'] - lang_code = s.get('language', 'auto') + if "Settings" in config: + s = config["Settings"] + lang_code = s.get("language", "auto") for i in range(self.lang_combo.count()): if self.lang_combo.itemData(i) == lang_code: - self.lang_combo.setCurrentIndex(i); break - - self.engine_combo.setCurrentText(s.get('engine', 'PyInstaller')) - - is_lite = s.getboolean('lite_mode', False) + self.lang_combo.setCurrentIndex(i) + break + + self.engine_combo.setCurrentText(s.get("engine", "PyInstaller")) + + is_lite = s.getboolean("lite_mode", False) self.rb_lite_mode.setChecked(is_lite) self.rb_compat_mode.setChecked(not is_lite) - - is_onefile = s.getboolean('onefile', True) + + is_onefile = s.getboolean("onefile", True) if is_onefile: self.rb_onefile.setChecked(True) else: self.rb_onedir.setChecked(True) - self.contents_dir_edit.setText(s.get('contents_dir', '_internal')) + self.contents_dir_edit.setText(s.get("contents_dir", "_internal")) self.on_mode_type_changed() - self.noconsole_check.setChecked(s.getboolean('noconsole', True)) - self.clean_all_check.setChecked(s.getboolean('clean_all', True)) - self.auto_icon_check.setChecked(s.getboolean('auto_icon', True)) - - pip_main = s.get('pip_index', 'https://pypi.org/simple') + self.noconsole_check.setChecked(s.getboolean("noconsole", True)) + self.clean_all_check.setChecked(s.getboolean("clean_all", True)) + self.auto_icon_check.setChecked(s.getboolean("auto_icon", True)) + + pip_main = s.get("pip_index", "https://pypi.org/simple") self._set_combo_value(self.pip_source_combo, pip_main) - pip_backup = s.get('pip_index_backup', 'https://test.pypi.org/simple') + pip_backup = s.get("pip_index_backup", "https://test.pypi.org/simple") self._set_combo_value(self.pip_backup_combo, pip_backup) - self.venv_check.setChecked(s.getboolean('use_venv', True)) - - self.keep_venv_check.setChecked(s.getboolean('keep_venv', False)) + self.venv_check.setChecked(s.getboolean("use_venv", True)) + + self.keep_venv_check.setChecked(s.getboolean("keep_venv", False)) self.keep_venv_check.setEnabled(self.venv_check.isChecked()) - - venv_mode = s.get('venv_mode', 'isolated') - if venv_mode == 'shared': + + venv_mode = s.get("venv_mode", "isolated") + if venv_mode == "shared": self.rb_venv_shared.setChecked(True) else: self.rb_venv_isolated.setChecked(True) - self.shared_venv_dir_edit.setText(s.get('shared_venv_dir', '')) - - self.reqs_check.setChecked(s.getboolean('use_reqs', True)) - self.pipreqs_check.setChecked(s.getboolean('use_pipreqs', True)) - self.pipreqs_dir_check.setChecked(s.getboolean('use_pipreqs_dir', False)) - self.reqs_file_edit.setText(s.get('use_reqs_file', '')) - self.python_path_combo.setCurrentText(s.get('custom_python_path', '')) - + self.shared_venv_dir_edit.setText(s.get("shared_venv_dir", "")) + + self.reqs_check.setChecked(s.getboolean("use_reqs", True)) + self.pipreqs_check.setChecked(s.getboolean("use_pipreqs", True)) + self.pipreqs_dir_check.setChecked(s.getboolean("use_pipreqs_dir", False)) + self.reqs_file_edit.setText(s.get("use_reqs_file", "")) + self.python_path_combo.setCurrentText(s.get("custom_python_path", "")) + if self.upx_check: - self.upx_check.setChecked(s.getboolean('upx', False)) + self.upx_check.setChecked(s.getboolean("upx", False)) self.on_upx_toggled(self.upx_check.isChecked()) - self.upx_path_edit.setText(s.get('upx_path', '')) - self.cores_spin.setValue(s.getint('cpu_cores', os.cpu_count() or 2)) - self.exclude_edit.setText(s.get('exclude_modules', '')) - self.out_mode_combo.setCurrentIndex(int(s.get('out_mode', '0'))) - self.out_dir_edit.setText(s.get('custom_out_dir', '')) - self.sandbox_mode_combo.setCurrentIndex(int(s.get('temp_sandbox_mode', '0'))) + self.upx_path_edit.setText(s.get("upx_path", "")) + self.cores_spin.setValue(s.getint("cpu_cores", os.cpu_count() or 2)) + self.exclude_edit.setText(s.get("exclude_modules", "")) + self.out_mode_combo.setCurrentIndex(int(s.get("out_mode", "0"))) + self.out_dir_edit.setText(s.get("custom_out_dir", "")) + self.sandbox_mode_combo.setCurrentIndex(int(s.get("temp_sandbox_mode", "0"))) self.on_out_mode_changed(self.out_mode_combo.currentIndex()) - - self.concise_log_check.setChecked(s.getboolean('concise_log', True)) - self.sound_notify_check.setChecked(s.getboolean('sound_notify', True)) - self.auto_save_log_check.setChecked(s.getboolean('auto_save_log', False)) - - self.pyi_ver_edit.setText(s.get('pyi_version', '6.21.0')) - self.nuitka_ver_edit.setText(s.get('nuitka_version', '4.1.3')) - default_sign_on_os = True if sys.platform == 'darwin' else False - self.enable_sign_check.setChecked(s.getboolean('enable_sign', default_sign_on_os)) + self.concise_log_check.setChecked(s.getboolean("concise_log", True)) + self.sound_notify_check.setChecked(s.getboolean("sound_notify", True)) + self.auto_save_log_check.setChecked(s.getboolean("auto_save_log", False)) - self.cert_path_edit.setText(s.get('cert_path', '')) - self.cert_pass_edit.setText(s.get('cert_pass', '')) + self.pyi_ver_edit.setText(s.get("pyi_version", "6.21.0")) + self.nuitka_ver_edit.setText(s.get("nuitka_version", "4.1.3")) + + default_sign_on_os = sys.platform == "darwin" + self.enable_sign_check.setChecked(s.getboolean("enable_sign", default_sign_on_os)) + + self.cert_path_edit.setText(s.get("cert_path", "")) + self.cert_pass_edit.setText(s.get("cert_pass", "")) if self.cert_path_edit.text(): self.adv_sign_container.setVisible(True) - self.btn_toggle_adv_sign.setText(_("Hide Custom Cert Settings ▾")) + self.btn_toggle_adv_sign.setText(_("Hide Custom Cert Settings ")) self.add_data_list.clear() - if 'Mappings' in config: - self.populate_mapping_table(dict(config['Mappings'])) + if "Mappings" in config: + self.populate_mapping_table(dict(config["Mappings"])) + + if "BackportRules" in config: + self.populate_backport_table(dict(config["BackportRules"])) + self.enable_shield_check.setChecked(s.getboolean("enable_backport_shield", True)) - if 'BackportRules' in config: - self.populate_backport_table(dict(config['BackportRules'])) - self.enable_shield_check.setChecked(s.getboolean('enable_backport_shield', True)) - def save_to_config(self): config = load_config() - if 'Settings' not in config: config['Settings'] = {} - s = config['Settings'] - + if "Settings" not in config: + config["Settings"] = {} + s = config["Settings"] + idx = self.lang_combo.currentIndex() new_lang = self.lang_combo.itemData(idx) - s['language'] = new_lang - - s['engine'] = self.engine_combo.currentText() - s['lite_mode'] = str(self.rb_lite_mode.isChecked()) - - s['onefile'] = str(self.rb_onefile.isChecked()) - s['contents_dir'] = self.contents_dir_edit.text().strip() or '_internal' - s['noconsole'] = str(self.noconsole_check.isChecked()) - s['clean_all'] = str(self.clean_all_check.isChecked()) - s['auto_icon'] = str(self.auto_icon_check.isChecked()) - - s['pip_index'] = self._get_url_from_combo(self.pip_source_combo) - s['pip_index_backup'] = self._get_url_from_combo(self.pip_backup_combo) + s["language"] = new_lang + + s["engine"] = self.engine_combo.currentText() + s["lite_mode"] = str(self.rb_lite_mode.isChecked()) + + s["onefile"] = str(self.rb_onefile.isChecked()) + s["contents_dir"] = self.contents_dir_edit.text().strip() or "_internal" + s["noconsole"] = str(self.noconsole_check.isChecked()) + s["clean_all"] = str(self.clean_all_check.isChecked()) + s["auto_icon"] = str(self.auto_icon_check.isChecked()) + + s["pip_index"] = self._get_url_from_combo(self.pip_source_combo) + s["pip_index_backup"] = self._get_url_from_combo(self.pip_backup_combo) + + s["use_venv"] = str(self.venv_check.isChecked()) + s["keep_venv"] = str(self.keep_venv_check.isChecked()) + s["venv_mode"] = "shared" if self.rb_venv_shared.isChecked() else "isolated" + s["shared_venv_dir"] = self.shared_venv_dir_edit.text().strip() + s["use_reqs"] = str(self.reqs_check.isChecked()) + s["use_pipreqs"] = str(self.pipreqs_check.isChecked()) + s["use_pipreqs_dir"] = str(self.pipreqs_dir_check.isChecked()) + s["use_reqs_file"] = self.reqs_file_edit.text().strip() - s['use_venv'] = str(self.venv_check.isChecked()) - s['keep_venv'] = str(self.keep_venv_check.isChecked()) - s['venv_mode'] = 'shared' if self.rb_venv_shared.isChecked() else 'isolated' - s['shared_venv_dir'] = self.shared_venv_dir_edit.text().strip() - s['use_reqs'] = str(self.reqs_check.isChecked()) - s['use_pipreqs'] = str(self.pipreqs_check.isChecked()) - s['use_pipreqs_dir'] = str(self.pipreqs_dir_check.isChecked()) - s['use_reqs_file'] = self.reqs_file_edit.text().strip() - raw_py = self.python_path_combo.currentText().strip() - if " (Python " in raw_py: raw_py = raw_py.split(" (Python ")[0].strip() - s['custom_python_path'] = raw_py - - if self.upx_check: s['upx'] = str(self.upx_check.isChecked()) - s['upx_path'] = self.upx_path_edit.text().strip() - s['cpu_cores'] = str(self.cores_spin.value()) - s['exclude_modules'] = self.exclude_edit.text().strip() - s['out_mode'] = str(self.out_mode_combo.currentIndex()) - s['custom_out_dir'] = self.out_dir_edit.text().strip() - s['temp_sandbox_mode'] = str(self.sandbox_mode_combo.currentIndex()) - s['concise_log'] = str(self.concise_log_check.isChecked()) - s['sound_notify'] = str(self.sound_notify_check.isChecked()) - s['auto_save_log'] = str(self.auto_save_log_check.isChecked()) - - s['pyi_version'] = self.pyi_ver_edit.text().strip() - s['nuitka_version'] = self.nuitka_ver_edit.text().strip() + if " (Python " in raw_py: + raw_py = raw_py.split(" (Python ")[0].strip() + s["custom_python_path"] = raw_py - s['enable_sign'] = str(self.enable_sign_check.isChecked()) - s['cert_path'] = self.cert_path_edit.text().strip() - s['cert_pass'] = self.cert_pass_edit.text().strip() + if self.upx_check: + s["upx"] = str(self.upx_check.isChecked()) + s["upx_path"] = self.upx_path_edit.text().strip() + s["cpu_cores"] = str(self.cores_spin.value()) + s["exclude_modules"] = self.exclude_edit.text().strip() + s["out_mode"] = str(self.out_mode_combo.currentIndex()) + s["custom_out_dir"] = self.out_dir_edit.text().strip() + s["temp_sandbox_mode"] = str(self.sandbox_mode_combo.currentIndex()) + s["concise_log"] = str(self.concise_log_check.isChecked()) + s["sound_notify"] = str(self.sound_notify_check.isChecked()) + s["auto_save_log"] = str(self.auto_save_log_check.isChecked()) - s['add_data_list'] = '' + s["pyi_version"] = self.pyi_ver_edit.text().strip() + s["nuitka_version"] = self.nuitka_ver_edit.text().strip() - config['Mappings'] = {} + s["enable_sign"] = str(self.enable_sign_check.isChecked()) + s["cert_path"] = self.cert_path_edit.text().strip() + s["cert_pass"] = self.cert_pass_edit.text().strip() + + s["add_data_list"] = "" + + config["Mappings"] = {} for r in range(self.mapping_table.rowCount()): item_k = self.mapping_table.item(r, 0) item_v = self.mapping_table.item(r, 1) k = item_k.text().strip() if item_k else "" v = item_v.text().strip() if item_v else "" - if k and v: config['Mappings'][k] = v + if k and v: + config["Mappings"][k] = v - s['enable_backport_shield'] = str(self.enable_shield_check.isChecked()) - config['BackportRules'] = {} + s["enable_backport_shield"] = str(self.enable_shield_check.isChecked()) + config["BackportRules"] = {} for r in range(self.backport_table.rowCount()): item_k = self.backport_table.item(r, 0) item_v = self.backport_table.item(r, 1) k = item_k.text().strip().lower() if item_k else "" v = item_v.text().strip() if item_v else "3.4" - if k: config['BackportRules'][k] = v + if k: + config["BackportRules"][k] = v save_config(config) I18N.set_language(new_lang) @@ -3847,12 +4429,12 @@ class SettingsPanel(QWidget): sender = self.sender() src_url = self._get_url_from_combo(self.pip_source_combo) bak_url = self._get_url_from_combo(self.pip_backup_combo) - - if src_url and bak_url and src_url.rstrip('/') == bak_url.rstrip('/'): + + if src_url and bak_url and src_url.rstrip("/") == bak_url.rstrip("/"): target_combo = self.pip_backup_combo if sender == self.pip_source_combo else self.pip_source_combo for i in range(target_combo.count()): item_url = target_combo.itemData(i) or target_combo.itemText(i) - if item_url and item_url.rstrip('/') != src_url.rstrip('/'): + if item_url and item_url.rstrip("/") != src_url.rstrip("/"): target_combo.blockSignals(True) target_combo.setCurrentIndex(i) target_combo.blockSignals(False) @@ -3861,62 +4443,75 @@ class SettingsPanel(QWidget): def on_engine_changed(self): engine = self.engine_combo.currentText() if engine == "PyInstaller": - self.engine_desc_lbl.setText(_("PyInstaller — Bundles Python interpreter and bytecode. Fast build speed, zero configuration (no C compiler needed), and excellent compatibility.")) + self.engine_desc_lbl.setText( + _( + "PyInstaller Bundles Python interpreter and bytecode. Fast build speed, zero configuration (no C compiler needed), and excellent compatibility." + ) + ) else: - self.engine_desc_lbl.setText(_("Nuitka — Compiles source code into native C/C++ binary. Produces smaller package size, faster execution, and deep anti-decompilation protection (requires C compiler).")) + self.engine_desc_lbl.setText( + _( + "Nuitka Compiles source code into native C/C++ binary. Produces smaller package size, faster execution, and deep anti-decompilation protection (requires C compiler)." + ) + ) - if getattr(self, 'upx_check', None) is not None and getattr(self, 'upx_path_container', None) is not None: + if getattr(self, "upx_check", None) is not None and getattr(self, "upx_path_container", None) is not None: self.upx_check.setVisible(True) self.upx_path_container.setVisible(self.upx_check.isChecked()) def on_python_path_changed(self, text=""): raw_text = text or self.python_path_combo.currentText().strip() - ver_match = re.search(r'Python\s+(\d+\.\d+)', raw_text, re.I) - + ver_match = re.search(r"Python\s+(\d+\.\d+)", raw_text, re.IGNORECASE) + ver_str = ver_match.group(1) if ver_match else "" if not ver_str: - m_path = re.search(r'python(\d)(\d+)', raw_text, re.I) - if m_path: ver_str = f"{m_path.group(1)}.{m_path.group(2)}" + m_path = re.search(r"python(\d)(\d+)", raw_text, re.IGNORECASE) + if m_path: + ver_str = f"{m_path.group(1)}.{m_path.group(2)}" - self.python_desc_lbl.setStyleSheet("QLabel { background-color: #f8fafc; color: #334155; border: 1px solid #e2e8f0; border-radius: 6px; padding: 10px 14px; font-size: 12px; line-height: 1.6; }") + self.python_desc_lbl.setStyleSheet( + "QLabel { background-color: #f8fafc; color: #334155; border: 1px solid #e2e8f0; border-radius: 6px; padding: 10px 14px; font-size: 12px; line-height: 1.6; }" + ) if ver_str: try: - parts = [int(x) for x in ver_str.split('.')] + parts = [int(x) for x in ver_str.split(".")] major, minor = parts[0], parts[1] - - html = f'
Python {ver_str} { _("OS Support Matrix:") }
' - + + html = f'
Python {ver_str} {_("OS Support Matrix:")}
' + tag_style = "background-color:#f1f5f9; color:#475569; padding:2px 8px; border-radius:4px; font-weight:bold; font-size:10px; margin-right: 8px;" - + if sys.platform == "win32": if (major, minor) <= (3, 8): - html += '✔ Windows 7 / 8 / 8.1    ✔ Windows 10 / 11    ✔ Windows Server 2008+
' + html += ' Windows 7 / 8 / 8.1    Windows 10 / 11    Windows Server 2008+
' html += f'
x86 / AMD64{_("Full backward compatibility")}
' elif (major, minor) <= (3, 10): - html += '✖ Windows 7    ✔ Windows 8.1    ✔ Windows 10 / 11    ✔ Windows Server 2012+
' + html += ' Windows 7    Windows 8.1    Windows 10 / 11    Windows Server 2012+
' html += f'
AMD64 / ARM64{_("No Windows 7 Support")}
' else: - html += '✖ Windows 7 / 8 / 8.1    ✔ Windows 10 / 11    ✔ Windows Server 2016+
' - html += f'
{_("* Nuitka Auto-manages C Compiler")}
' - + html += ' Windows 7 / 8 / 8.1    Windows 10 / 11    Windows Server 2016+
' + html += ( + f'
{_("* Nuitka Auto-manages C Compiler")}
' + ) + elif sys.platform == "darwin": if (major, minor) <= (3, 8): - html += '✔ OS X 10.9 (Mavericks) +    ✔ macOS 10.15 (Catalina)
' + html += ' OS X 10.9 (Mavericks) +    macOS 10.15 (Catalina)
' html += f'
x86_64 Intel Only{_("Legacy macOS Compatible")}
' elif (major, minor) <= (3, 10): - html += '✔ macOS 10.9 (Mavericks) +    ✔ macOS 11 (Big Sur) +
' + html += ' macOS 10.9 (Mavericks) +    macOS 11 (Big Sur) +
' html += f'
Universal2 Binary{_("Apple Silicon (M1/M2) & Intel")}
' else: - html += '✔ macOS 10.13 (High Sierra) +    ✔ macOS 13 / 14 / 15+
' + html += ' macOS 10.13 (High Sierra) +    macOS 13 / 14 / 15+
' html += f'
Universal2 Binary{_("Apple Silicon (M-Series) & Intel")}
' - + else: if (major, minor) <= (3, 9): - html += '✔ Ubuntu 16.04+    ✔ CentOS / RHEL 7+    ✔ Debian 9+
' + html += ' Ubuntu 16.04+    CentOS / RHEL 7+    Debian 9+
' html += f'
x86_64 / aarch64glibc >= 2.17
' else: - html += '✔ Ubuntu 20.04+    ✔ Fedora 32+    ✔ Debian 11+    ✔ RHEL 8+
' + html += ' Ubuntu 20.04+    Fedora 32+    Debian 11+    RHEL 8+
' html += f'
x86_64 / aarch64glibc >= 2.28
' self.python_desc_lbl.setText(html) @@ -3924,28 +4519,37 @@ class SettingsPanel(QWidget): except Exception: pass - self.python_desc_lbl.setText(_('
Interpreter
Auto-detecting system environment and OS support matrix...')) + self.python_desc_lbl.setText( + _( + '
Interpreter
Auto-detecting system environment and OS support matrix...' + ) + ) def on_upx_toggled(self, checked): - if getattr(self, 'upx_path_container', None) is not None: + if getattr(self, "upx_path_container", None) is not None: self.upx_path_container.setVisible(checked) def on_out_mode_changed(self, index): - show_custom = (index == 1) - if getattr(self, 'out_dir_container', None) is not None: + show_custom = index == 1 + if getattr(self, "out_dir_container", None) is not None: self.out_dir_container.setVisible(show_custom) def select_out_dir(self): d = QFileDialog.getExistingDirectory(self, _("Target Directory:"), options=QFileDialog.Option.DontUseNativeDialog) - if d: self.out_dir_edit.setText(Path(d).resolve().as_posix()) + if d: + self.out_dir_edit.setText(Path(d).resolve().as_posix()) def select_upx_path(self): d = QFileDialog.getExistingDirectory(self, _("UPX Path:"), options=QFileDialog.Option.DontUseNativeDialog) - if d: self.upx_path_edit.setText(Path(d).resolve().as_posix()) + if d: + self.upx_path_edit.setText(Path(d).resolve().as_posix()) def select_reqs_file(self): - f, _filter = QFileDialog.getOpenFileName(self, _("Requirements File:"), "", "Requirements Files (*.txt);;All Files (*)", options=QFileDialog.Option.DontUseNativeDialog) - if f: self.reqs_file_edit.setText(Path(f).resolve().as_posix()) + f, _filter = QFileDialog.getOpenFileName( + self, _("Requirements File:"), "", "Requirements Files (*.txt);;All Files (*)", options=QFileDialog.Option.DontUseNativeDialog + ) + if f: + self.reqs_file_edit.setText(Path(f).resolve().as_posix()) def select_shared_venv_dir(self): d = QFileDialog.getExistingDirectory(self, _("Select shared directory..."), options=QFileDialog.Option.DontUseNativeDialog) @@ -3959,7 +4563,7 @@ class SettingsPanel(QWidget): self.rb_venv_isolated.setChecked(True) def clean_local_venvs(self): - script_path = getattr(self.parent_win, 'script_path', '') + script_path = getattr(self.parent_win, "script_path", "") target_dir = Path(script_path).parent if (script_path and Path(script_path).exists()) else Path.cwd() self.btn_clean_venvs.setEnabled(False) @@ -3974,26 +4578,29 @@ class SettingsPanel(QWidget): self.btn_clean_venvs.setText(_("Clear Local Venvs")) if not venvs_to_remove: - if hasattr(self.parent_win, 'show_notification'): + if hasattr(self.parent_win, "show_notification"): self.parent_win.show_notification(_("No local virtual environments found to clean.")) return count = len(venvs_to_remove) - freed_mb = freed_bytes / (1024 * 1024) + freed_bytes / (1024 * 1024) paths_html = "
".join([f"• {v.name}" for v in venvs_to_remove]) msg_box = QMessageBox(self) msg_box.setWindowTitle(_("Clear Virtual Environments")) - msg_box.setText(_( - "Found {count} virtual environment(s) for the current project:
" - "{paths}

" - "Are you sure you want to delete them to free up disk space?", - count=count, paths=paths_html - )) + msg_box.setText( + _( + "Found {count} virtual environment(s) for the current project:
" + "{paths}

" + "Are you sure you want to delete them to free up disk space?", + count=count, + paths=paths_html, + ) + ) msg_box.setIcon(QMessageBox.Icon.Warning) btn_confirm = msg_box.addButton(_("Delete All"), QMessageBox.ButtonRole.AcceptRole) - btn_cancel = msg_box.addButton(_("Cancel"), QMessageBox.ButtonRole.RejectRole) + msg_box.addButton(_("Cancel"), QMessageBox.ButtonRole.RejectRole) msg_box.setStyleSheet(""" QMessageBox { background-color: #ffffff; } @@ -4027,75 +4634,85 @@ class SettingsPanel(QWidget): def _on_venv_delete_done(self, success_count, freed_mb): self.btn_clean_venvs.setEnabled(True) self.btn_clean_venvs.setText(_("Clear Local Venvs")) - - msg = _("Successfully cleaned {count} virtual environment(s), freed {size:.1f} MB disk space.", - count=success_count, size=freed_mb) - if hasattr(self.parent_win, 'show_notification'): + + msg = _("Successfully cleaned {count} virtual environment(s), freed {size:.1f} MB disk space.", count=success_count, size=freed_mb) + if hasattr(self.parent_win, "show_notification"): self.parent_win.show_notification(msg, 6000) def select_python_path(self): - exe_filter = "Executable (*.exe);;All Files (*)" if os.name == 'nt' else "All Files (*)" + exe_filter = "Executable (*.exe);;All Files (*)" if os.name == "nt" else "All Files (*)" f, _filter = QFileDialog.getOpenFileName(self, _("Interpreter:"), "", exe_filter, options=QFileDialog.Option.DontUseNativeDialog) - if f: self.python_path_combo.setCurrentText(Path(f).resolve().as_posix()) + if f: + self.python_path_combo.setCurrentText(Path(f).resolve().as_posix()) def on_download_python_clicked(self): - if hasattr(self, 'parent_win') and hasattr(self.parent_win, "_trigger_python_download_dialog"): + if hasattr(self, "parent_win") and hasattr(self.parent_win, "_trigger_python_download_dialog"): self.parent_win._trigger_python_download_dialog(is_missing_mode=False) def update_icon_preview(self, path): if path and Path(path).exists(): pixmap = QPixmap(path) if not pixmap.isNull(): - self.icon_preview.setPixmap(pixmap.scaled(28, 28, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)) + self.icon_preview.setPixmap( + pixmap.scaled(28, 28, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) + ) return self.icon_preview.clear() def select_icon(self): - p, _filter = QFileDialog.getOpenFileName(self, _("App Icon:"), "", "Icon Files (*.ico *.svg *.png *.icns)", options=QFileDialog.Option.DontUseNativeDialog) - if p: self.icon_edit.setText(Path(p).resolve().as_posix()) + p, _filter = QFileDialog.getOpenFileName( + self, _("App Icon:"), "", "Icon Files (*.ico *.svg *.png *.icns)", options=QFileDialog.Option.DontUseNativeDialog + ) + if p: + self.icon_edit.setText(Path(p).resolve().as_posix()) def auto_scan_hidden(self): script_path = self.parent_win.script_path - if not hasattr(self.parent_win, "show_notification"): return - + if not hasattr(self.parent_win, "show_notification"): + return + if not script_path: return self.parent_win.show_error_log(_("[ERROR] Please load a valid Python source file first!")) if is_cloud_locked(script_path): - return self.parent_win.show_error_log(_("[ERROR] Target file is locked or encrypted by cloud drive. Please decrypt and try again.")) - + return self.parent_win.show_error_log( + _("[ERROR] Target file is locked or encrypted by cloud drive. Please decrypt and try again.") + ) + try: scan_dir = self.pipreqs_dir_check.isChecked() target = Path(script_path).parent if scan_dir else Path(script_path) hidden = extract_project_imports_via_ast(target, scan_dir=scan_dir) hidden = [m for m in hidden if m not in STD_LIBS] - self.hidden_edit.setText(','.join(sorted(hidden))) + self.hidden_edit.setText(",".join(sorted(hidden))) self.parent_win.show_notification(_("AST scan completed, found {count} dependencies.", count=len(hidden))) - except Exception as e: + except Exception as e: self.parent_win.show_error_log(_("[ERROR] Exception occurred during AST parsing: {error}", error=str(e))) def on_resources_dropped(self, paths): for p_str in paths: p = Path(p_str).resolve() if p.is_file(): - self._add_resource_item('file', p.as_posix(), ".") + self._add_resource_item("file", p.as_posix(), ".") elif p.is_dir(): - self._add_resource_item('dir', p.as_posix(), p.name) + self._add_resource_item("dir", p.as_posix(), p.name) def add_resource_files(self): - files, _filter = QFileDialog.getOpenFileNames(self, _("Add File"), "", "All Files (*)", options=QFileDialog.Option.DontUseNativeDialog) + files, _filter = QFileDialog.getOpenFileNames( + self, _("Add File"), "", "All Files (*)", options=QFileDialog.Option.DontUseNativeDialog + ) for f in files: src = Path(f).resolve().as_posix() - self._add_resource_item('file', src, ".") - + self._add_resource_item("file", src, ".") + def add_resource_dir(self): folder = QFileDialog.getExistingDirectory(self, _("Add Dir"), options=QFileDialog.Option.DontUseNativeDialog) if folder: src = Path(folder).resolve().as_posix() dst = Path(folder).name - self._add_resource_item('dir', src, dst) - + self._add_resource_item("dir", src, dst) + def _add_resource_item(self, r_type, src, dst): - tag = _("File") if r_type == 'file' else _("Directory") + tag = _("File") if r_type == "file" else _("Directory") display_text = f"[{tag}] {src} -> {dst}" item = QListWidgetItem(display_text) item.setData(Qt.ItemDataRole.UserRole, (r_type, src, dst)) @@ -4130,16 +4747,19 @@ class SettingsPanel(QWidget): menu.exec(line_edit.mapToGlobal(pos)) def edit_resource(self, item=None): - if not item: item = self.add_data_list.currentItem() - if not item: return + if not item: + item = self.add_data_list.currentItem() + if not item: + return r_type, src, dst = item.data(Qt.ItemDataRole.UserRole) - + dlg = CustomInputDialog(self, _("Edit Path"), _("Target relative path:"), text=dst) if dlg.exec() == QDialog.DialogCode.Accepted: - new_dst = dlg.get_text().strip().replace('\\', '/') - if not new_dst: new_dst = "." + new_dst = dlg.get_text().strip().replace("\\", "/") + if not new_dst: + new_dst = "." item.setData(Qt.ItemDataRole.UserRole, (r_type, src, new_dst)) - tag = _("File") if r_type == 'file' else _("Directory") + tag = _("File") if r_type == "file" else _("Directory") item.setText(f"[{tag}] {src} -> {new_dst}") def del_resource(self): @@ -4152,16 +4772,18 @@ class SettingsPanel(QWidget): def reset_global_config(self): msg_box = QMessageBox(self) msg_box.setWindowTitle(_("Reset to Default Config")) - msg_box.setText(_( - "Are you sure you want to reset all preferences?
" - "" - "All settings will be restored to default state." - "" - )) + msg_box.setText( + _( + "Are you sure you want to reset all preferences?
" + "" + "All settings will be restored to default state." + "" + ) + ) msg_box.setIcon(QMessageBox.Icon.Warning) btn_confirm = msg_box.addButton(_("Reset"), QMessageBox.ButtonRole.AcceptRole) - btn_cancel = msg_box.addButton(_("Cancel"), QMessageBox.ButtonRole.RejectRole) + msg_box.addButton(_("Cancel"), QMessageBox.ButtonRole.RejectRole) msg_box.setStyleSheet(""" QMessageBox { background-color: #ffffff; } @@ -4198,6 +4820,7 @@ class SettingsPanel(QWidget): if hasattr(self.parent_win, "show_notification"): self.parent_win.show_notification(_("Global configuration has been reset.")) + class ScriptAnalysisThread(QThread): analysis_done = Signal(str, str, str, str, set) @@ -4206,8 +4829,9 @@ class ScriptAnalysisThread(QThread): self.path = path def run(self): - if self.isInterruptionRequested(): return - + if self.isInterruptionRequested(): + return + app_name = Path(self.path).stem version = "" author = "Independent Developer" @@ -4216,35 +4840,44 @@ class ScriptAnalysisThread(QThread): try: content = safe_read_text(Path(self.path))[:10240] - - v_match = re.search(r'^(?:__version__|VERSION|version)\s*=\s*[\'"]([^\'"]+)[\'"]', content, re.M | re.I) - if v_match: version = v_match.group(1) - - c_match = re.search(r'^(?:__company__|COMPANY)\s*=\s*[\'"]([^\'"]+)[\'"]', content, re.M | re.I) - if c_match: + v_match = re.search(r'^(?:__version__|VERSION|version)\s*=\s*[\'"]([^\'"]+)[\'"]', content, re.MULTILINE | re.IGNORECASE) + + if v_match: + version = v_match.group(1) + + c_match = re.search(r'^(?:__company__|COMPANY)\s*=\s*[\'"]([^\'"]+)[\'"]', content, re.MULTILINE | re.IGNORECASE) + if c_match: author = c_match.group(1) else: - a_match = re.search(r'^(?:__author__|AUTHOR)\s*=\s*[\'"]([^\'"]+)[\'"]', content, re.M | re.I) - if a_match: author = a_match.group(1) - - n_match = re.search(r'^(?:__title__|__app_name__|APP_NAME)\s*=\s*[\'"]([^\'"]+)[\'"]', content, re.M | re.I) - if n_match: app_name = n_match.group(1) - - d_match = re.search(r'^(?:__description__|DESCRIPTION)\s*=\s*[\'"]([^\'"]+)[\'"]', content, re.M | re.I) - if d_match: desc = d_match.group(1) - except: pass + a_match = re.search(r'^(?:__author__|AUTHOR)\s*=\s*[\'"]([^\'"]+)[\'"]', content, re.MULTILINE | re.IGNORECASE) + if a_match: + author = a_match.group(1) - if self.isInterruptionRequested(): return + n_match = re.search(r'^(?:__title__|__app_name__|APP_NAME)\s*=\s*[\'"]([^\'"]+)[\'"]', content, re.MULTILINE | re.IGNORECASE) + if n_match: + app_name = n_match.group(1) + + d_match = re.search(r'^(?:__description__|DESCRIPTION)\s*=\s*[\'"]([^\'"]+)[\'"]', content, re.MULTILINE | re.IGNORECASE) + if d_match: + desc = d_match.group(1) + except Exception: + pass + + if self.isInterruptionRequested(): + return try: script_imports = extract_project_imports_via_ast(Path(self.path), False) - except: pass + except Exception: + pass - if self.isInterruptionRequested(): return + if self.isInterruptionRequested(): + return self.analysis_done.emit(app_name, version, author, desc, script_imports) + class PackingThread(QThread): progress = Signal(str) build_finished = Signal(bool, str, list) @@ -4258,29 +4891,29 @@ class PackingThread(QThread): self.temp_workpath = None self.temp_out_dir = None self.temp_dist_dir = None - self.all_raw_logs = [] + self.all_raw_logs = [] def cancel(self): self._is_cancelled = True if not self.process: return - + try: if os.name == "nt": subprocess.run( - ["taskkill", "/F", "/T", "/PID", str(self.process.pid)], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + ["taskkill", "/F", "/T", "/PID", str(self.process.pid)], + capture_output=True, timeout=3, - creationflags=subprocess.CREATE_NO_WINDOW + creationflags=subprocess.CREATE_NO_WINDOW, ) else: import signal + try: os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) except ProcessLookupError: pass - + try: self.process.wait(timeout=PROCESS_KILL_TIMEOUT) except subprocess.TimeoutExpired: @@ -4289,24 +4922,25 @@ class PackingThread(QThread): self.process.wait(timeout=2) except Exception as e: logger.warning(f"Force kill failed: {e}") - + except Exception as e: logger.error(f"Process cancel failed: {e}\n{traceback.format_exc()}") finally: self._is_cancelled = True def run_cmd(self, cmd, cwd=None, timeout=None, silent_error=False, extra_env=None): - if self._is_cancelled: return False - + if self._is_cancelled: + return False + timer = None is_timeout = [False] - + clean_env = os.environ.copy() clean_env.pop("PYTHONHOME", None) - + for c_var in ("VIRTUAL_ENV", "CONDA_PREFIX", "CONDA_DEFAULT_ENV", "CONDA_PYTHON_EXE", "CONDA_PROMPT_MODIFIER"): clean_env.pop(c_var, None) - + if not (extra_env and "PYTHONPATH" in extra_env): if not ("PYTHONPATH" in clean_env and "_qpypack_temp_" in clean_env["PYTHONPATH"]): clean_env.pop("PYTHONPATH", None) @@ -4323,32 +4957,46 @@ class PackingThread(QThread): clean_env.update(extra_env) try: - kwargs = {"stdout": subprocess.PIPE, "stderr": subprocess.STDOUT, "cwd": cwd, - "text": True, "encoding": "utf-8", "errors": "replace", "env": clean_env} - if os.name == 'nt': + kwargs = { + "stdout": subprocess.PIPE, + "stderr": subprocess.STDOUT, + "cwd": cwd, + "text": True, + "encoding": "utf-8", + "errors": "replace", + "env": clean_env, + } + if os.name == "nt": kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW else: kwargs["start_new_session"] = True - + self.process = subprocess.Popen(cmd, **kwargs) - + if timeout: + def kill_proc(): is_timeout[0] = True try: if os.name == "nt": - subprocess.run(["taskkill", "/F", "/T", "/PID", str(self.process.pid)], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags=subprocess.CREATE_NO_WINDOW) + subprocess.run( + ["taskkill", "/F", "/T", "/PID", str(self.process.pid)], + capture_output=True, + creationflags=subprocess.CREATE_NO_WINDOW, + ) else: import signal + os.killpg(os.getpgid(self.process.pid), signal.SIGKILL) - except: pass + except Exception: + pass + timer = threading.Timer(timeout, kill_proc) timer.start() buffer = [] last_emit = time.time() - + for line in self.process.stdout: if self._is_cancelled: try: @@ -4357,34 +5005,34 @@ class PackingThread(QThread): except Exception: pass return False - - stripped = line.rstrip('\r\n') + + stripped = line.rstrip("\r\n") self.all_raw_logs.append(stripped) if silent_error: continue - + buffer.append(stripped) if len(buffer) >= 15 or (time.time() - last_emit) > 0.1: - self.progress.emit('\n'.join(buffer)) + self.progress.emit("\n".join(buffer)) buffer.clear() last_emit = time.time() - + if buffer and not silent_error: - self.progress.emit('\n'.join(buffer)) + self.progress.emit("\n".join(buffer)) self.process.wait() - + if is_timeout[0]: self.progress.emit(_("[WARN] Command timeout (>{timeout}s)", timeout=timeout)) return False - + return self.process.returncode == 0 except FileNotFoundError as e: error_msg = _("[ERROR] Process error: command or binary missing ({error})", error=str(e)) self.progress.emit(error_msg) logger.error(f"Command not found: {cmd[0] if cmd else 'unknown'}, Error: {e}") return False - except subprocess.TimeoutExpired as e: + except subprocess.TimeoutExpired: self.progress.emit(_("[WARN] Command timeout (>{timeout}s)", timeout=timeout)) logger.warning(f"Command timeout: {' '.join(cmd[:3])}, Timeout: {timeout}s") return False @@ -4394,7 +5042,8 @@ class PackingThread(QThread): logger.error(f"Unexpected error in run_cmd: {e}\n{traceback.format_exc()}") return False finally: - if timer: timer.cancel() + if timer: + timer.cancel() if self.process: if self.process.stdout: try: @@ -4407,29 +5056,29 @@ class PackingThread(QThread): logger.debug(f"Process wait failed: {e}") def run_pip_install(self, python_exe, pkgs_or_args): - primary_idx = self.params.get('pip_index_url', '').strip() - backup_idx = self.params.get('pip_index_backup', '').strip() + primary_idx = self.params.get("pip_index_url", "").strip() + backup_idx = self.params.get("pip_index_backup", "").strip() - pip_args = [ - "--default-timeout=120", - "--disable-pip-version-check", - "--prefer-binary" - ] - if primary_idx: pip_args.extend(["-i", primary_idx]) - if backup_idx and backup_idx != primary_idx: pip_args.extend(["--extra-index-url", backup_idx]) + pip_args = ["--default-timeout=120", "--disable-pip-version-check", "--prefer-binary"] + if primary_idx: + pip_args.extend(["-i", primary_idx]) + if backup_idx and backup_idx != primary_idx: + pip_args.extend(["--extra-index-url", backup_idx]) cmd = [python_exe, "-m", "pip", "install"] + pkgs_or_args + pip_args success = self.run_cmd(cmd) - if not success and not self.params.get('use_venv') and not self._is_cancelled: + if not success and not self.params.get("use_venv") and not self._is_cancelled: self.progress.emit(_("[WARN] System Python write permission restricted. Retrying with '--user' mode...")) user_cmd = [python_exe, "-m", "pip", "install", "--user"] + pkgs_or_args + pip_args success = self.run_cmd(user_cmd) if not success and backup_idx and backup_idx != primary_idx: self.progress.emit(_("[INFO] Switching to backup PyPI source for retrieval: {url}", url=backup_idx)) - fallback_cmd = [python_exe, "-m", "pip", "install"] + pkgs_or_args + ["-i", backup_idx, "--disable-pip-version-check", "--prefer-binary"] - if not self.params.get('use_venv'): + fallback_cmd = ( + [python_exe, "-m", "pip", "install"] + pkgs_or_args + ["-i", backup_idx, "--disable-pip-version-check", "--prefer-binary"] + ) + if not self.params.get("use_venv"): fallback_cmd.insert(4, "--user") success = self.run_cmd(fallback_cmd) @@ -4438,13 +5087,13 @@ class PackingThread(QThread): def sanitize_script(self, orig_path: Path): if is_cloud_locked(orig_path): return None, False, _("[ERROR] Target file is locked or encrypted by cloud drive. Please decrypt and try again.") - - if not self.params['noconsole']: + + if not self.params["noconsole"]: try: code = safe_read_text(orig_path) - - pause_prompt_str = _("\\nProgram execution completed, press Enter to exit...").replace('\\n', '\n') - + + pause_prompt_str = _("\\nProgram execution completed, press Enter to exit...").replace("\\n", "\n") + pause_code = ( "# --- QPyPack Auto-injected Console Pause ---\n" "import atexit\n" @@ -4454,83 +5103,77 @@ class PackingThread(QThread): " if sys.platform == 'win32':\n" " import ctypes\n" " if ctypes.windll.kernel32.GetConsoleProcessList((ctypes.c_uint * 10)(), 10) <= 2:\n" - f" input({repr(pause_prompt_str)})\n" - " except:\n" + f" input({pause_prompt_str!r})\n" + " except Exception:\n" " pass\n" "atexit.register(_qpypack_pause)\n" "# -------------------------------------------\n\n" ) - + temp_file = orig_path.parent / f"_qpypack_temp_{orig_path.name}" try: - temp_file.write_text(pause_code + code, encoding='utf-8') + temp_file.write_text(pause_code + code, encoding="utf-8") except PermissionError: temp_file = Path(tempfile.gettempdir()) / f"_qpypack_temp_{orig_path.name}" - temp_file.write_text(pause_code + code, encoding='utf-8') + temp_file.write_text(pause_code + code, encoding="utf-8") os.environ["PYTHONPATH"] = orig_path.parent.as_posix() + os.pathsep + os.environ.get("PYTHONPATH", "") - + return temp_file, True, "" except Exception as e: self.progress.emit(_("[WARN] Pause code injection exception: {error}", error=str(e))) - + return orig_path, False, "" def detect_python_syntax_errors(self): - script_path = self.params['script_path'] + script_path = self.params["script_path"] script_name = Path(script_path).name log_text = "\n".join(self.all_raw_logs) - - file_line_pat = re.compile(r'File "([^"]+)", line (\d+)', re.I) - err_type_pat = re.compile(r'^(IndentationError|SyntaxError|TabError):\s*(.*)', re.M) - + + file_line_pat = re.compile(r'File "([^"]+)", line (\d+)', re.IGNORECASE) + err_type_pat = re.compile(r"^(IndentationError|SyntaxError|TabError):\s*(.*)", re.MULTILINE) + err_matches = list(err_type_pat.finditer(log_text)) if err_matches: last_err = err_matches[-1] err_type = last_err.group(1) err_desc = last_err.group(2) - + err_pos = last_err.start() line_no = _("Unknown") file_name = script_name - + file_line_matches = list(file_line_pat.finditer(log_text)) for m in reversed(file_line_matches): if m.end() < err_pos: matched_filepath = m.group(1) - if matched_filepath.endswith(('.py', '.pyw')): + if matched_filepath.endswith((".py", ".pyw")): line_no = m.group(2) file_name = Path(matched_filepath).name break - - return { - "is_code_error": True, - "type": err_type, - "desc": err_desc, - "line": line_no, - "file": file_name - } + + return {"is_code_error": True, "type": err_type, "desc": err_desc, "line": line_no, "file": file_name} return {"is_code_error": False} def auto_sign_executable(self, target_path: Path) -> bool: - is_macos = (sys.platform == 'darwin') - enable_sign_setting = self.params.get('enable_sign', False) + is_macos = sys.platform == "darwin" + enable_sign_setting = self.params.get("enable_sign", False) if not is_macos and not enable_sign_setting: return True self.progress.emit(_("[INFO] Applying smart digital signature...")) - if os.name == 'nt': + if os.name == "nt": if target_path.is_file(): for _wait in range(6): try: - with open(target_path, 'r+b'): + with open(target_path, "r+b"): break - except (IOError, OSError): + except OSError: time.sleep(0.5) - cert_path = self.params.get('cert_path', '').strip() - cert_pass = self.params.get('cert_pass', '').strip() + cert_path = self.params.get("cert_path", "").strip() + cert_pass = self.params.get("cert_pass", "").strip() targets = [] if target_path.is_dir(): @@ -4559,9 +5202,7 @@ class PackingThread(QThread): signed_with_ts = False if signtool_exe: - signed_with_ts = self._sign_with_signtool( - signtool_exe, target_str, cert_path, cert_pass, tsa_servers - ) + signed_with_ts = self._sign_with_signtool(signtool_exe, target_str, cert_path, cert_pass, tsa_servers) if signed_with_ts and target_file == targets[0]: self.progress.emit(_("[INFO] Applied digital signature with timestamp ({tsa})...", tsa="RFC3161/signtool")) @@ -4615,23 +5256,20 @@ class PackingThread(QThread): thumbprint = None if not (cert_path and Path(cert_path).exists()): - company_name = self.params.get('ver_comp', '').strip() or 'QPyPack App' + company_name = self.params.get("ver_comp", "").strip() or "QPyPack App" safe_company = company_name.replace("'", "''").replace('"', '""') ps_create = ( f'$subject = "CN={safe_company}"; ' - '$cert = Get-ChildItem Cert:\\CurrentUser\\My -CodeSigningCert | Where-Object { $_.Subject -eq $subject } | Select-Object -First 1; ' - 'if (-not $cert) { $cert = New-SelfSignedCertificate -Type CodeSigningCert -Subject $subject -CertStoreLocation Cert:\\CurrentUser\\My }; ' - 'Write-Output $cert.Thumbprint' + "$cert = Get-ChildItem Cert:\\CurrentUser\\My -CodeSigningCert | Where-Object { $_.Subject -eq $subject } | Select-Object -First 1; " + "if (-not $cert) { $cert = New-SelfSignedCertificate -Type CodeSigningCert -Subject $subject -CertStoreLocation Cert:\\CurrentUser\\My }; " + "Write-Output $cert.Thumbprint" ) try: kw = {"capture_output": True, "text": True, "timeout": 10, "errors": "ignore"} - if os.name == 'nt': + if os.name == "nt": kw["creationflags"] = subprocess.CREATE_NO_WINDOW - res = subprocess.run( - ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", ps_create], - **kw - ) - thumbprint = res.stdout.strip().split('\n')[-1].strip() if res.returncode == 0 else "" + res = subprocess.run(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", ps_create], **kw) + thumbprint = res.stdout.strip().split("\n")[-1].strip() if res.returncode == 0 else "" if not thumbprint: return False except Exception: @@ -4642,14 +5280,12 @@ class PackingThread(QThread): return False if cert_path and Path(cert_path).exists(): - cmd = [signtool_exe, "sign", "/f", cert_path, "/fd", "sha256", - "/tr", tsa, "/td", "sha256"] + cmd = [signtool_exe, "sign", "/f", cert_path, "/fd", "sha256", "/tr", tsa, "/td", "sha256"] if cert_pass: cmd.extend(["/p", cert_pass]) cmd.append(target_path) else: - cmd = [signtool_exe, "sign", "/sha1", thumbprint, "/fd", "sha256", - "/tr", tsa, "/td", "sha256", target_path] + cmd = [signtool_exe, "sign", "/sha1", thumbprint, "/fd", "sha256", "/tr", tsa, "/td", "sha256", target_path] if self.run_cmd(cmd, silent_error=True, timeout=30): return True @@ -4657,24 +5293,24 @@ class PackingThread(QThread): return False def _sign_with_powershell(self, target_path, cert_path, cert_pass, tsa_servers, is_first=False) -> bool: - company_name = self.params.get('ver_comp', '').strip() + company_name = self.params.get("ver_comp", "").strip() if not company_name: - app_title = self.params.get('app_name', 'App').strip() or 'App' + app_title = self.params.get("app_name", "App").strip() or "App" company_name = f"{app_title} Developer" safe_company = company_name.replace("'", "''").replace('"', '""') if cert_path and Path(cert_path).exists(): - safe_pwd = (cert_pass or '').replace("'", "''") - pass_part = f' -Password (ConvertTo-SecureString -String \'{safe_pwd}\' -Force -AsPlainText)' if cert_pass else '' + safe_pwd = (cert_pass or "").replace("'", "''") + pass_part = f" -Password (ConvertTo-SecureString -String '{safe_pwd}' -Force -AsPlainText)" if cert_pass else "" base_cert_script = f'$cert = Get-PfxCertificate -FilePath "{cert_path}"{pass_part}; ' else: base_cert_script = ( f'$subject = "CN={safe_company}"; ' - '$cert = Get-ChildItem Cert:\\CurrentUser\\My -CodeSigningCert | Where-Object { $_.Subject -eq $subject } | Select-Object -First 1; ' - 'if (-not $cert) { ' - '$cert = New-SelfSignedCertificate -Type CodeSigningCert -Subject $subject -CertStoreLocation Cert:\\CurrentUser\\My ' - '}; ' + "$cert = Get-ChildItem Cert:\\CurrentUser\\My -CodeSigningCert | Where-Object { $_.Subject -eq $subject } | Select-Object -First 1; " + "if (-not $cert) { " + "$cert = New-SelfSignedCertificate -Type CodeSigningCert -Subject $subject -CertStoreLocation Cert:\\CurrentUser\\My " + "}; " ) for tsa in tsa_servers: @@ -4682,14 +5318,13 @@ class PackingThread(QThread): return False ps_cmd = ( - f'{base_cert_script}' + f"{base_cert_script}" f'$sig = Set-AuthenticodeSignature -FilePath "{target_path}" -Certificate $cert ' f'-TimestampServer "{tsa}" -HashAlgorithm SHA256; ' f'if ($sig.Status -ne "Valid" -and $sig.Status -ne "UnknownError") {{ exit 1 }}; ' - f'if ($null -eq $sig.TimeStamperCertificate) {{ exit 1 }}' + f"if ($null -eq $sig.TimeStamperCertificate) {{ exit 1 }}" ) - if self.run_cmd(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", ps_cmd], - silent_error=True, timeout=30): + if self.run_cmd(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", ps_cmd], silent_error=True, timeout=30): if is_first: self.progress.emit(_("[INFO] Applied digital signature with timestamp ({tsa})...", tsa=tsa)) return True @@ -4697,26 +5332,27 @@ class PackingThread(QThread): self.progress.emit(_("[WARN] Timestamp server connection timed out, performing local fast signing...")) ps_cmd_no_ts = ( - f'{base_cert_script}' + f"{base_cert_script}" f'$sig = Set-AuthenticodeSignature -FilePath "{target_path}" -Certificate $cert -HashAlgorithm SHA256; ' f'if ($sig.Status -ne "Valid" -and $sig.Status -ne "UnknownError") {{ exit 1 }}' ) - return self.run_cmd(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", ps_cmd_no_ts], - silent_error=True, timeout=15) + return self.run_cmd( + ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", ps_cmd_no_ts], silent_error=True, timeout=15 + ) def run(self): - engine = self.params['engine'] - app_name = self.params.get('app_name', 'app').strip() or 'app' - pip_idx = self.params.get('pip_index_url', '').strip() - pip_backup = self.params.get('pip_index_backup', '').strip() + engine = self.params["engine"] + app_name = self.params.get("app_name", "app").strip() or "app" + pip_idx = self.params.get("pip_index_url", "").strip() + self.params.get("pip_index_backup", "").strip() is_temp = False build_script_path = None ext = ".exe" if os.name == "nt" else "" failed_packages = [] - - script_dir = Path(self.params['script_path']).parent if self.params.get('script_path') else Path.cwd() - sandbox_mode = int(self.params.get('temp_sandbox_mode', 0) or 0) + script_dir = Path(self.params["script_path"]).parent if self.params.get("script_path") else Path.cwd() + + sandbox_mode = int(self.params.get("temp_sandbox_mode", 0) or 0) if sandbox_mode == 0: custom_temp_base = (script_dir / ".qpypack_build").resolve() custom_temp_base.mkdir(parents=True, exist_ok=True) @@ -4731,6 +5367,7 @@ class PackingThread(QThread): try: import platform + os_info = f"{platform.system()} {platform.release()} ({platform.machine()})" py_arch = "64-bit" if sys.maxsize > 2**32 else "32-bit" diag_msg = ( @@ -4749,14 +5386,15 @@ class PackingThread(QThread): pass self.progress.emit(_("[INFO] Analyzing source code and project dependencies...")) - script_path = Path(self.params['script_path']).resolve() + script_path = Path(self.params["script_path"]).resolve() script_dir = script_path.parent - system_python_exe = self.params.get('python_exe') + system_python_exe = self.params.get("python_exe") if system_python_exe and os.path.exists(system_python_exe): try: kw_chk = {"capture_output": True, "text": True, "timeout": 5, "errors": "ignore"} - if os.name == 'nt': kw_chk["creationflags"] = subprocess.CREATE_NO_WINDOW + if os.name == "nt": + kw_chk["creationflags"] = subprocess.CREATE_NO_WINDOW chk_syntax = subprocess.run([system_python_exe, "-m", "py_compile", script_path.as_posix()], **kw_chk) if chk_syntax.returncode != 0: err_desc = chk_syntax.stderr.strip() or _("Invalid syntax") @@ -4768,7 +5406,8 @@ class PackingThread(QThread): "Please ensure the [Build Python Version] you selected matches the version you used to [Write/Test the Code].\n" "Using newer syntax (e.g., walrus operator :=, type unions |, match-case) in an older Python environment will trigger this error.\n" "We recommend going to [Build Settings] -> [Engine] to switch to the correct Python version.", - file=script_path.name, desc=err_desc + file=script_path.name, + desc=err_desc, ) return self.build_finished.emit(False, msg, []) except Exception: @@ -4795,11 +5434,11 @@ class PackingThread(QThread): health_issues.append(_("Python interpreter not found: {path}", path=system_python_exe or "None")) if health_issues: - error_msg = _("[ERROR] Pre-flight check failed:") + "\n" + "\n".join(f" • {issue}" for issue in health_issues) + error_msg = _("[ERROR] Pre-flight check failed:") + "\n" + "\n".join(f" {issue}" for issue in health_issues) return self.build_finished.emit(False, error_msg, []) - out_mode = int(self.params.get('out_mode', 0) or 0) - custom_out = (self.params.get('custom_out_dir') or '').strip() + out_mode = int(self.params.get("out_mode", 0) or 0) + custom_out = (self.params.get("custom_out_dir") or "").strip() target_out_dir = Path(custom_out) if (out_mode == 1 and custom_out) else script_dir try: target_out_dir.mkdir(parents=True, exist_ok=True) @@ -4807,22 +5446,28 @@ class PackingThread(QThread): test_file.write_text("test") test_file.unlink() except Exception as e: - return self.build_finished.emit(False, _("[ERROR] Output directory is missing write permissions: {error}", error=str(e)), []) + return self.build_finished.emit( + False, _("[ERROR] Output directory is missing write permissions: {error}", error=str(e)), [] + ) - if self.params.get('use_reqs'): - custom_reqs = (self.params.get('reqs_file') or '').strip() + if self.params.get("use_reqs"): + custom_reqs = (self.params.get("reqs_file") or "").strip() if custom_reqs and not Path(custom_reqs).exists(): return self.build_finished.emit(False, _("[ERROR] Requirements file not found: {path}", path=custom_reqs), []) - for r_type, src, dst in (self.params.get('add_data_list') or []): + for _r_type, src, _dst in self.params.get("add_data_list") or []: if not Path(src).exists(): return self.build_finished.emit(False, _("[ERROR] Additional resource file/directory not found: {path}", path=src), []) if is_cloud_sync_path(script_dir): - self.progress.emit(_("[WARN] Target project is in a Cloud Sync directory (e.g. OneDrive/Dropbox). Cloud sync may temporarily lock build files.")) + self.progress.emit( + _( + "[WARN] Target project is in a Cloud Sync directory (e.g. OneDrive/Dropbox). Cloud sync may temporarily lock build files." + ) + ) build_script_path, is_temp, err_msg = self.sanitize_script(script_path) - if not build_script_path and err_msg: + if not build_script_path and err_msg: return self.build_finished.emit(False, _("[ERROR] I/O Exception: {err_msg}", err_msg=err_msg), []) script_posix = build_script_path.as_posix() @@ -4832,29 +5477,30 @@ class PackingThread(QThread): except Exception as e: self.progress.emit(_("[WARN] AST Analysis Exception: {error}", error=str(e))) - known_mappings = self.params.get('mappings', DEFAULT_MAPPINGS.copy()) + known_mappings = self.params.get("mappings", DEFAULT_MAPPINGS.copy()) known_mappings_lower = {k.lower(): v for k, v in known_mappings.items()} def get_canonical_pypi_name(raw_name): - clean = raw_name.strip().lower().replace('_', '-') + clean = raw_name.strip().lower().replace("_", "-") return known_mappings_lower.get(clean, clean).lower() target_std_libs = set(STD_LIBS) try: cmd_std = [system_python_exe, "-c", "import sys; print(','.join(getattr(sys, 'stdlib_module_names', [])))"] kw = {"capture_output": True, "text": True, "timeout": 3, "errors": "ignore"} - if os.name == 'nt': kw["creationflags"] = subprocess.CREATE_NO_WINDOW + if os.name == "nt": + kw["creationflags"] = subprocess.CREATE_NO_WINDOW res = subprocess.run(cmd_std, **kw) if res.returncode == 0 and res.stdout.strip(): - target_std_libs.update(res.stdout.strip().split(',')) + target_std_libs.update(res.stdout.strip().split(",")) except Exception: pass def parse_req_line(line): line = line.strip() - if not line or line.startswith('#') or line.startswith('-'): + if not line or line.startswith(("#", "-")): return None, None - m = re.match(r'^([a-zA-Z0-9_\-\.]+)(.*)$', line) + m = re.match(r"^([a-zA-Z0-9_\-\.]+)(.*)$", line) if m: pkg_raw = m.group(1) canon_name = get_canonical_pypi_name(pkg_raw) @@ -4872,14 +5518,15 @@ class PackingThread(QThread): auto_detected_pkgs = set() auto_added_supplements = set() - if self.params.get('use_reqs'): - custom_reqs = self.params.get('reqs_file', '').strip() + if self.params.get("use_reqs"): + custom_reqs = self.params.get("reqs_file", "").strip() req_file = Path(custom_reqs) if (custom_reqs and Path(custom_reqs).exists()) else (script_dir / "requirements.txt") if req_file.exists(): try: - if is_cloud_locked(req_file): raise ValueError("Requirements file is locked") + if is_cloud_locked(req_file): + raise ValueError("Requirements file is locked") req_content = safe_read_text(req_file) - + for line in req_content.splitlines(): canon_name, full_spec = parse_req_line(line) @@ -4889,13 +5536,18 @@ class PackingThread(QThread): except Exception as e: self.progress.emit(_("[WARN] Read requirements.txt warning: {error}", error=str(e))) - local_modules = {p.stem.lower() for p in script_dir.iterdir() - if (p.is_file() and p.suffix.lower() in ('.py', '.pyw', '.pyd', '.so')) or (p.is_dir() and (p / '__init__.py').exists())} + local_modules = { + p.stem.lower() + for p in script_dir.iterdir() + if (p.is_file() and p.suffix.lower() in (".py", ".pyw", ".pyd", ".so")) or (p.is_dir() and (p / "__init__.py").exists()) + } for m in script_imports: - if m.lower() in target_std_libs or m.lower() in local_modules: continue + if m.lower() in target_std_libs or m.lower() in local_modules: + continue canon_name = get_canonical_pypi_name(m) - if canon_name.lower() in target_std_libs: continue + if canon_name.lower() in target_std_libs: + continue auto_detected_pkgs.add(canon_name) if canon_name not in final_dependencies: final_dependencies[canon_name] = canon_name @@ -4907,18 +5559,19 @@ class PackingThread(QThread): self.progress.emit(_("[INFO] Initializing isolated build environment...")) self.progress.emit(_("[INFO] Python interpreter path: {path}", path=system_python_exe)) - if self.params['use_venv']: + if self.params["use_venv"]: if self._is_cancelled: return self.build_finished.emit(False, _("[INFO] Build Cancelled."), []) - + import hashlib - + target_py_ver = "3.x" target_py_arch = "x64" if sys.maxsize > 2**32 else "x86" try: kw_check = {"capture_output": True, "text": True, "timeout": 4, "errors": "ignore"} - if os.name == 'nt': kw_check["creationflags"] = subprocess.CREATE_NO_WINDOW - + if os.name == "nt": + kw_check["creationflags"] = subprocess.CREATE_NO_WINDOW + code_info = "import sys, platform; print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}|{platform.architecture()[0]}')" res_info = subprocess.run([system_python_exe, "-c", code_info], **kw_check) if res_info.returncode == 0 and "|" in res_info.stdout: @@ -4929,15 +5582,15 @@ class PackingThread(QThread): self.progress.emit(_("[WARN] Python environment info detection fallback: {error}", error=str(e))) raw_stem = build_script_path.stem - clean_stem = re.sub(r'[^a-zA-Z0-9]', '', raw_stem) or "script" + clean_stem = re.sub(r"[^a-zA-Z0-9]", "", raw_stem) or "script" short_stem = clean_stem[:12] - path_hash = hashlib.md5(script_path.resolve().as_posix().encode('utf-8')).hexdigest()[:6] - + path_hash = hashlib.sha256(script_path.resolve().as_posix().encode("utf-8"), usedforsecurity=False).hexdigest()[:6] + safe_venv_name = f".qpypack_venv_{short_stem}_{path_hash}_py{target_py_ver}_{target_py_arch}" - if self.params.get('keep_venv'): - if self.params.get('venv_mode') == 'shared': - shared_dir = self.params.get('shared_venv_dir', '').strip() + if self.params.get("keep_venv"): + if self.params.get("venv_mode") == "shared": + shared_dir = self.params.get("shared_venv_dir", "").strip() if shared_dir and Path(shared_dir).exists(): try: test_file = Path(shared_dir) / ".qpypack_write_test" @@ -4957,16 +5610,17 @@ class PackingThread(QThread): self.venv_dir = Path(tempfile.mkdtemp(prefix=f"qpypack_env_{short_stem}_", dir=temp_dir_arg)).resolve() python_exe_in_venv = (self.venv_dir / ("Scripts/python.exe" if os.name == "nt" else "bin/python")).as_posix() - + is_venv_valid = False - if self.params.get('keep_venv') and self.venv_dir.exists() and Path(python_exe_in_venv).exists(): + if self.params.get("keep_venv") and self.venv_dir.exists() and Path(python_exe_in_venv).exists(): try: kw_val = {"capture_output": True, "text": True, "timeout": 5, "errors": "ignore"} - if os.name == 'nt': kw_val["creationflags"] = subprocess.CREATE_NO_WINDOW - + if os.name == "nt": + kw_val["creationflags"] = subprocess.CREATE_NO_WINDOW + code_val = "import sys, pip; print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')" check_proc = subprocess.run([python_exe_in_venv, "-c", code_val], **kw_val) - + if check_proc.returncode == 0 and check_proc.stdout.strip() == target_py_ver: is_venv_valid = True except Exception: @@ -4980,7 +5634,7 @@ class PackingThread(QThread): try: robust_rmtree(self.venv_dir) except Exception as e: - if self.params.get('keep_venv'): + if self.params.get("keep_venv"): self.progress.emit(_("[WARN] Failed to delete invalid venv, attempting to rename: {error}", error=str(e))) try: backup_name = f"{self.venv_dir.name}_backup_{int(time.time())}" @@ -4994,47 +5648,58 @@ class PackingThread(QThread): " Error: {error}\n\n" "Please manually delete the directory or uncheck 'Keep Local Venv' and retry.", path=self.venv_dir.as_posix(), - error=str(rename_err) + error=str(rename_err), ) return self.build_finished.emit(False, error_msg, []) else: - self.progress.emit(_("[WARN] Failed to purge invalid environment, falling back to temp sandbox: {error}", error=str(e))) + self.progress.emit( + _("[WARN] Failed to purge invalid environment, falling back to temp sandbox: {error}", error=str(e)) + ) self.venv_dir = Path(tempfile.mkdtemp(prefix=f"qpypack_env_{short_stem}_", dir=temp_dir_arg)).resolve() - python_exe_in_venv = (self.venv_dir / ("Scripts/python.exe" if os.name == "nt" else "bin/python")).as_posix() + python_exe_in_venv = ( + self.venv_dir / ("Scripts/python.exe" if os.name == "nt" else "bin/python") + ).as_posix() self.progress.emit(_("[INFO] Creating virtual environment ({name})...", name=self.venv_dir.name)) if not self.run_cmd([system_python_exe, "-m", "venv", self.venv_dir.as_posix()]): if self._is_cancelled: return self.build_finished.emit(False, _("[INFO] Build Cancelled."), []) - return self.build_finished.emit(False, _("[ERROR] Failed to create virtual environment. Current Python environment might be missing necessary modules or have restricted permissions."), []) - - if self.params.get('keep_venv') and self.venv_dir.exists(): + return self.build_finished.emit( + False, + _( + "[ERROR] Failed to create virtual environment. Current Python environment might be missing necessary modules or have restricted permissions." + ), + [], + ) + + if self.params.get("keep_venv") and self.venv_dir.exists(): try: (self.venv_dir / ".gitignore").write_text("*\n", encoding="utf-8") - except Exception: pass - + except Exception: + pass + python_exe = python_exe_in_venv - + if self._is_cancelled: return self.build_finished.emit(False, _("[INFO] Build Cancelled."), []) self.progress.emit(_("[INFO] Synchronizing and upgrading pip package manager...")) self.run_pip_install(python_exe, ["--upgrade", "pip", "-q"]) - else: + else: python_exe = system_python_exe if self._is_cancelled: return self.build_finished.emit(False, _("[INFO] Build Cancelled."), []) self.progress.emit(_("[INFO] Scanning project source code via AST engine...")) - - scan_dir_mode = self.params.get('use_pipreqs_dir', False) + + scan_dir_mode = self.params.get("use_pipreqs_dir", False) if scan_dir_mode: ast_discovered_imports = extract_project_imports_via_ast(script_dir, scan_dir=True) else: ast_discovered_imports = script_imports - + env_pkg_map = query_target_env_packages(system_python_exe) - + for m in ast_discovered_imports: if m.lower() in target_std_libs or m.lower() in local_modules: continue @@ -5045,15 +5710,14 @@ class PackingThread(QThread): final_dependencies[canon_name] = canon_name auto_added_supplements.add(canon_name) - engine_pkg = "pyinstaller" if engine == "PyInstaller" else "nuitka" - if engine == "PyInstaller" and self.params.get('pyi_version'): + if engine == "PyInstaller" and self.params.get("pyi_version"): engine_pkg = f"pyinstaller=={self.params['pyi_version']}" - elif engine == "Nuitka" and self.params.get('nuitka_version'): + elif engine == "Nuitka" and self.params.get("nuitka_version"): engine_pkg = f"nuitka=={self.params['nuitka_version']}" - + engine_pkgs = [engine_pkg] - if engine == "PyInstaller": + if engine == "PyInstaller": engine_pkgs.append("pillow") engine_pkgs.append("pyinstaller-hooks-contrib") elif engine == "Nuitka": @@ -5061,71 +5725,126 @@ class PackingThread(QThread): self.progress.emit(_("[INFO] Resolving project dependencies...")) if reqs_declared_pkgs: - self.progress.emit(" • " + _("Declared in requirements.txt ({count}): {pkgs}", count=len(reqs_declared_pkgs), pkgs=', '.join(sorted(reqs_declared_pkgs)))) + self.progress.emit( + " " + + _( + "Declared in requirements.txt ({count}): {pkgs}", + count=len(reqs_declared_pkgs), + pkgs=", ".join(sorted(reqs_declared_pkgs)), + ) + ) + self.progress.emit( + " " + + _( + "Discovered via scanner ({count}): {pkgs}", + count=len(auto_detected_pkgs), + pkgs=", ".join(sorted(auto_detected_pkgs)), + ) + ) + if auto_added_supplements: + self.progress.emit( + " " + + _( + "Auto-patched missing ({count}): {pkgs}", + count=len(auto_added_supplements), + pkgs=", ".join(sorted(auto_added_supplements)), + ) + ) + else: + self.progress.emit(" " + _("Manifest complete (No missing packages)")) else: - self.progress.emit(" • " + _("Declared in requirements.txt: None")) - - self.progress.emit(" • " + _("Discovered via scanner ({count}): {pkgs}", count=len(auto_detected_pkgs), pkgs=', '.join(sorted(auto_detected_pkgs)))) - - if auto_added_supplements: - self.progress.emit(" • " + _("Auto-patched missing ({count}): {pkgs}", count=len(auto_added_supplements), pkgs=', '.join(sorted(auto_added_supplements)))) - else: - self.progress.emit(" • " + _("Manifest complete (No missing packages)")) + self.progress.emit( + " " + + _( + "Discovered via scanner ({count}): {pkgs}", + count=len(auto_detected_pkgs), + pkgs=", ".join(sorted(auto_detected_pkgs)), + ) + ) - self.progress.emit(" • " + _("Build engine packages ({count}): {pkgs}", count=len(engine_pkgs), pkgs=', '.join(sorted(engine_pkgs)))) + self.progress.emit( + " " + _("Build engine packages ({count}): {pkgs}", count=len(engine_pkgs), pkgs=", ".join(sorted(engine_pkgs))) + ) final_install_dict = {} for spec in engine_pkgs + list(final_dependencies.values()): - m = re.match(r'^([a-zA-Z0-9_\-\.]+)(.*)$', spec.strip()) + m = re.match(r"^([a-zA-Z0-9_\-\.]+)(.*)$", spec.strip()) if m: base_name = m.group(1).lower() has_ver = bool(m.group(2).strip()) if base_name in final_install_dict: - existing_has_ver = bool(re.match(r'^[a-zA-Z0-9_\-\.]+[=>= min_ver_tuple: + for pkg_name, min_ver_raw in user_backport_rules.items(): + if isinstance(min_ver_raw, (tuple, list)): + min_ver_tup = (int(min_ver_raw[0]), int(min_ver_raw[1])) if len(min_ver_raw) >= 2 else (int(min_ver_raw[0]), 0) + elif isinstance(min_ver_raw, str): + try: + parts = [int(x) for x in min_ver_raw.strip().split(".")] + min_ver_tup = (parts[0], parts[1]) if len(parts) >= 2 else (parts[0], 0) + except Exception: + min_ver_tup = (3, 4) + else: + min_ver_tup = (3, 4) + + if target_py_ver >= min_ver_tup: active_exclusions.add(pkg_name.lower()) sanitized_dependencies = {} for canon_name, full_spec in final_dependencies.items(): clean_k = canon_name.lower().strip() if clean_k in active_exclusions: - self.progress.emit(_("[INFO] Auto-shielded obsolete backport module: '{pkg}' (Built-in in Python {ver})", - pkg=canon_name, ver=f"{target_py_ver[0]}.{target_py_ver[1]}")) + self.progress.emit( + _( + "[INFO] Auto-shielded obsolete backport module: '{pkg}' (Built-in in Python {ver})", + pkg=canon_name, + ver=f"{target_py_ver[0]}.{target_py_ver[1]}", + ) + ) continue sanitized_dependencies[canon_name] = full_spec final_dependencies = sanitized_dependencies temp_unified_reqs = custom_temp_base / f"qpypack_atomic_reqs_{int(time.time())}.txt" - temp_unified_reqs.write_text('\n'.join(dedup_install_list), encoding='utf-8') - + temp_unified_reqs.write_text("\n".join(dedup_install_list), encoding="utf-8") total_pkgs = len(dedup_install_list) - pkg_names_str = ', '.join(sorted(dedup_install_list)) - - self.progress.emit(_("[INFO] Installing build environment and project dependencies ({count} packages): {pkgs}", count=total_pkgs, pkgs=pkg_names_str)) + pkg_names_str = ", ".join(sorted(dedup_install_list)) + + self.progress.emit( + _( + "[INFO] Installing build environment and project dependencies ({count} packages): {pkgs}", + count=total_pkgs, + pkgs=pkg_names_str, + ) + ) if self._is_cancelled: temp_unified_reqs.unlink(missing_ok=True) @@ -5136,10 +5855,12 @@ class PackingThread(QThread): temp_unified_reqs.unlink(missing_ok=True) return self.build_finished.emit(False, _("[INFO] Build Cancelled."), []) - self.progress.emit(_("[WARN] Specified versions failed to install. Stripping version constraints for automatic compatibility match...")) - - flex_install_list = [re.split(r'[=> 2**32}')"], **kw_v) + if os.name == "nt": + kw_v["creationflags"] = subprocess.CREATE_NO_WINDOW + check_ver = subprocess.run( + [ + python_exe, + "-c", + "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}|{sys.maxsize > 2**32}')", + ], + **kw_v, + ) ver_str = check_ver.stdout.strip() if ver_str and "|" in ver_str: - v_part, arch_part = ver_str.split('|') - parts = v_part.split('.') + v_part, arch_part = ver_str.split("|") + parts = v_part.split(".") py_ver_num = (int(parts[0]), int(parts[1])) - is_64bit = (arch_part.lower() == 'true') - except Exception: pass + is_64bit = arch_part.lower() == "true" + except Exception: + pass has_suitable_msvc = False has_win_sdk = False - vswhere = Path(os.environ.get("ProgramFiles(x86)", "C:\\Program Files (x86)")) / "Microsoft Visual Studio/Installer/vswhere.exe" - + vswhere = ( + Path(os.environ.get("ProgramFiles(x86)", "C:\\Program Files (x86)")) + / "Microsoft Visual Studio/Installer/vswhere.exe" + ) + if vswhere.exists(): try: min_vs_ver = "17.0" if py_ver_num >= (3, 11) else "15.0" - res_vc = subprocess.run([ - vswhere.as_posix(), "-latest", "-version", min_vs_ver, - "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64" - ], capture_output=True, text=True, errors="ignore", creationflags=subprocess.CREATE_NO_WINDOW) + res_vc = subprocess.run( + [ + vswhere.as_posix(), + "-latest", + "-version", + min_vs_ver, + "-requires", + "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + ], + capture_output=True, + text=True, + errors="ignore", + creationflags=subprocess.CREATE_NO_WINDOW, + ) if res_vc.stdout.strip(): has_suitable_msvc = True - except Exception: pass + except Exception: + pass sdk_paths = [ Path(os.environ.get("ProgramFiles(x86)", "C:\\Program Files (x86)")) / "Windows Kits" / "10" / "Lib", @@ -5375,156 +6157,262 @@ class PackingThread(QThread): if has_suitable_msvc and has_win_sdk: cmd.append("--msvc=latest") self.progress.emit(_("[INFO] Found compatible MSVC + Windows SDK environment, using native compiler.")) - + elif py_ver_num >= (3, 13) and is_64bit: cmd.append("--zig") self.progress.emit(_("[INFO] Python 3.13+ detected: Using Zig compiler (--zig) as native backend.")) - + elif py_ver_num <= (3, 12): cmd.append("--mingw64") if not has_suitable_msvc and py_ver_num >= (3, 11): - self.progress.emit(_("[INFO] MSVC 14.3+ not detected. Nuitka will use/download MinGW64 toolchain (Python {v}).", v=f"{py_ver_num[0]}.{py_ver_num[1]}")) + self.progress.emit( + _( + "[INFO] MSVC 14.3+ not detected. Nuitka will use/download MinGW64 toolchain (Python {v}).", + v=f"{py_ver_num[0]}.{py_ver_num[1]}", + ) + ) else: self.progress.emit(_("[INFO] Using MinGW64 compiler.")) - + else: cmd.append("--msvc=latest") - self.progress.emit(_("[WARN] Python 3.13+ (32-bit) requires Visual Studio 2022 with Windows SDK. Build will attempt with current environment.")) + self.progress.emit( + _( + "[WARN] Python 3.13+ (32-bit) requires Visual Studio 2022 with Windows SDK. Build will attempt with current environment." + ) + ) if free_disk < 3.0: - self.progress.emit(_("[WARN] Low disk space detected on build target drive (Available: {free:.1f} GB, >= 5.0 GB recommended). Build process may interrupt due to disk exhaustion.", free=free_disk)) + self.progress.emit( + _( + "[WARN] Low disk space detected on build target drive (Available: {free:.1f} GB, >= 5.0 GB recommended). Build process may interrupt due to disk exhaustion.", + free=free_disk, + ) + ) - cores = self.params.get('cpu_cores', os.cpu_count() or 2) + cores = self.params.get("cpu_cores", os.cpu_count() or 2) free_ram = get_free_ram_gb() if free_ram < 2.0 and cores > 1: safe_jobs = 1 - self.progress.emit(_("[INFO] Evaluating system physical memory (Available: {ram:.1f} GB). Adaptive concurrency adjusted: {cores} -> {safe_jobs} ...", ram=free_ram, cores=cores, safe_jobs=safe_jobs)) + self.progress.emit( + _( + "[INFO] Evaluating system physical memory (Available: {ram:.1f} GB). Adaptive concurrency adjusted: {cores} -> {safe_jobs} ...", + ram=free_ram, + cores=cores, + safe_jobs=safe_jobs, + ) + ) cores = safe_jobs cmd.append("--low-memory") elif free_ram < 3.5 and cores > 2: safe_jobs = max(2, min(cores, int(free_ram))) - self.progress.emit(_("[INFO] Evaluating system physical memory (Available: {ram:.1f} GB). Adaptive concurrency adjusted: {cores} -> {safe_jobs} ...", ram=free_ram, cores=cores, safe_jobs=safe_jobs)) + self.progress.emit( + _( + "[INFO] Evaluating system physical memory (Available: {ram:.1f} GB). Adaptive concurrency adjusted: {cores} -> {safe_jobs} ...", + ram=free_ram, + cores=cores, + safe_jobs=safe_jobs, + ) + ) cores = safe_jobs cmd.append("--low-memory") elif free_ram < 6.0 and cores > 4: safe_jobs = max(3, min(cores, int(free_ram / 1.5))) if safe_jobs < cores: - self.progress.emit(_("[INFO] Evaluating system physical memory (Available: {ram:.1f} GB). Adaptive concurrency adjusted: {cores} -> {safe_jobs} ...", ram=free_ram, cores=cores, safe_jobs=safe_jobs)) + self.progress.emit( + _( + "[INFO] Evaluating system physical memory (Available: {ram:.1f} GB). Adaptive concurrency adjusted: {cores} -> {safe_jobs} ...", + ram=free_ram, + cores=cores, + safe_jobs=safe_jobs, + ) + ) cores = safe_jobs cmd.append(f"--jobs={cores}") - - if self.params.get('upx'): + + if self.params.get("upx"): cmd.append("--enable-plugin=upx") - upx_dir_custom = (self.params.get('upx_path') or '').strip() + upx_dir_custom = (self.params.get("upx_path") or "").strip() if upx_dir_custom and Path(upx_dir_custom).exists(): upx_exe = Path(upx_dir_custom) / ("upx.exe" if os.name == "nt" else "upx") if upx_exe.exists(): cmd.append(f"--upx-binary={upx_exe.as_posix()}") else: cmd.append(f"--upx-binary={upx_dir_custom}") - - if self.params['noconsole']: + + if self.params["noconsole"]: if os.name == "nt": cmd.append("--windows-console-mode=disable") if sys.platform == "darwin": cmd.append("--macos-create-app-bundle") else: - if os.name == "nt": + if os.name == "nt": cmd.append("--windows-console-mode=force") - if self.params['onefile']: + if self.params["onefile"]: cmd.append("--onefile") - else: + else: cmd.append("--standalone") - if icon_path: - if os.name == "nt": cmd.append(f"--windows-icon-from-ico={icon_path}") - if sys.platform == "darwin": cmd.append(f"--macos-app-icon={icon_path}") + if icon_path: + if os.name == "nt": + cmd.append(f"--windows-icon-from-ico={icon_path}") + if sys.platform == "darwin": + cmd.append(f"--macos-app-icon={icon_path}") cmd.append(f"--include-data-files={Path(icon_path).resolve().as_posix()}={Path(icon_path).name}") - + if os.name == "nt": - if self.params.get('ver_comp'): cmd.append(f"--company-name={self.params['ver_comp']}") - if self.params.get('ver_desc'): cmd.append(f"--file-description={self.params['ver_desc']}") - if self.params.get('app_name'): cmd.append(f"--product-name={self.params['app_name']}") - if self.params.get('ver_ver'): - v_str = self.params['ver_ver'].strip() - v_nums = re.findall(r'\d+', v_str) - v_clean = ".".join((v_nums + ['0', '0', '0', '0'])[:4]) + if self.params.get("ver_comp"): + cmd.append(f"--company-name={self.params['ver_comp']}") + if self.params.get("ver_desc"): + cmd.append(f"--file-description={self.params['ver_desc']}") + if self.params.get("app_name"): + cmd.append(f"--product-name={self.params['app_name']}") + if self.params.get("ver_ver"): + v_str = self.params["ver_ver"].strip() + v_nums = re.findall(r"\d+", v_str) + v_clean = ".".join((v_nums + ["0", "0", "0", "0"])[:4]) cmd.append(f"--file-version={v_clean}") cmd.append(f"--product-version={v_clean}") elif sys.platform == "darwin": - if self.params.get('ver_comp'): cmd.append(f"--company-name={self.params['ver_comp']}") - if self.params.get('ver_ver'): cmd.append(f"--macos-app-version={self.params['ver_ver']}") - comp = self.params.get('ver_comp', 'mycompany').strip().lower().replace(" ", "") + if self.params.get("ver_comp"): + cmd.append(f"--company-name={self.params['ver_comp']}") + if self.params.get("ver_ver"): + cmd.append(f"--macos-app-version={self.params['ver_ver']}") + comp = self.params.get("ver_comp", "mycompany").strip().lower().replace(" ", "") bundle_id = f"com.{comp or 'anonymous'}.{app_name.lower().replace(' ', '')}" cmd.append(f"--macos-signed-app-name={bundle_id}") - hidden_list = [i.strip().lower() for i in (self.params.get('hidden_imports') or '').split(',') if i.strip()] + hidden_list = [i.strip().lower() for i in (self.params.get("hidden_imports") or "").split(",") if i.strip()] imports_lower = {m.lower() for m in script_imports} | set(hidden_list) - if 'pyqt5' in imports_lower: cmd.append("--enable-plugin=pyqt5") - elif 'pyqt6' in imports_lower: cmd.append("--enable-plugin=pyqt6") - elif 'pyside2' in imports_lower: cmd.append("--enable-plugin=pyside2") - elif 'pyside6' in imports_lower: cmd.append("--enable-plugin=pyside6") - - if 'matplotlib' in imports_lower: cmd.append("--enable-plugin=matplotlib") - if any(tk in imports_lower for tk in ('tkinter', 'pysimplegui', 'customtkinter', 'turtle', 'easygui', 'ttkbootstrap')): + if "pyqt5" in imports_lower: + cmd.append("--enable-plugin=pyqt5") + elif "pyqt6" in imports_lower: + cmd.append("--enable-plugin=pyqt6") + elif "pyside2" in imports_lower: + cmd.append("--enable-plugin=pyside2") + elif "pyside6" in imports_lower: + cmd.append("--enable-plugin=pyside6") + + if "matplotlib" in imports_lower: + cmd.append("--enable-plugin=matplotlib") + if any(tk in imports_lower for tk in ("tkinter", "pysimplegui", "customtkinter", "turtle", "easygui", "ttkbootstrap")): cmd.append("--enable-plugin=tk-inter") - if 'multiprocessing' in imports_lower: + if "multiprocessing" in imports_lower: cmd.append("--enable-plugin=multiprocessing") if os.name == "nt": - self.progress.emit(_("[WARN] 'multiprocessing' module detected. Ensure 'multiprocessing.freeze_support()' is called under 'if __name__ == \"__main__\":' to prevent infinite process loops (fork bombs).")) - is_lite = self.params.get('lite_mode') - - if 'ttkbootstrap' in imports_lower: - if not is_lite: cmd.append("--include-package=ttkbootstrap") + self.progress.emit( + _( + "[WARN] 'multiprocessing' module detected. Ensure 'multiprocessing.freeze_support()' is called under 'if __name__ == \"__main__\":' to prevent infinite process loops (fork bombs)." + ) + ) + is_lite = self.params.get("lite_mode") + + if "ttkbootstrap" in imports_lower: + if not is_lite: + cmd.append("--include-package=ttkbootstrap") cmd.append("--include-package-data=ttkbootstrap") - - if 'playwright' in imports_lower or has_playwright_pkg: - if not is_lite: cmd.append("--include-package=playwright") + + if "playwright" in imports_lower or has_playwright_pkg: + if not is_lite: + cmd.append("--include-package=playwright") cmd.append("--include-package-data=playwright") - if any(lib in imports_lower for lib in ('requests', 'httpx', 'urllib3', 'aiohttp')): + if any(lib in imports_lower for lib in ("requests", "httpx", "urllib3", "aiohttp")): cmd.append("--include-package-data=certifi") - self.progress.emit(_("[WARN] Detected 'requests' or 'httpx'. Auto-bundling 'certifi' certificates to prevent SSL errors.")) - - for web_fw in ('fastapi', 'uvicorn', 'flask', 'streamlit', 'pywebio', 'dash', 'pyecharts', 'pyppeteer'): + self.progress.emit( + _("[WARN] Detected 'requests' or 'httpx'. Auto-bundling 'certifi' certificates to prevent SSL errors.") + ) + + for web_fw in ("fastapi", "uvicorn", "flask", "streamlit", "pywebio", "dash", "pyecharts", "pyppeteer"): if web_fw in imports_lower: - if not is_lite: cmd.append(f"--include-package={web_fw}") + if not is_lite: + cmd.append(f"--include-package={web_fw}") cmd.append(f"--include-package-data={web_fw}") - if 'numpy' in imports_lower: cmd.append("--enable-plugin=numpy") + if "numpy" in imports_lower: + cmd.append("--enable-plugin=numpy") - for imp in (self.params.get('hidden_imports') or '').split(','): - if imp.strip(): cmd.extend([f"--include-module={imp.strip()}"]) - - for r_type, src, dst in (self.params.get('add_data_list') or []): + for imp in (self.params.get("hidden_imports") or "").split(","): + if imp.strip(): + cmd.extend([f"--include-module={imp.strip()}"]) + + for r_type, src, dst in self.params.get("add_data_list") or []: src_path = Path(src).resolve().as_posix() - if r_type == 'dir': + if r_type == "dir": cmd.append(f"--include-data-dir={src_path}={dst}") else: filename = Path(src).name - if dst == ".": nuitka_dst = filename - else: nuitka_dst = os.path.normpath(os.path.join(dst, filename)).replace('\\', '/') + if dst == ".": + nuitka_dst = filename + else: + nuitka_dst = os.path.normpath(os.path.join(dst, filename)).replace("\\", "/") cmd.append(f"--include-data-files={src_path}={nuitka_dst}") - for excl in (self.params.get('exclude_modules') or '').split(','): - if excl.strip(): cmd.append(f"--nofollow-import-to={excl.strip()}") + for excl in (self.params.get("exclude_modules") or "").split(","): + if excl.strip(): + cmd.append(f"--nofollow-import-to={excl.strip()}") - if self.params.get('lite_mode'): + if self.params.get("lite_mode"): self.progress.emit(_("[INFO] Lite mode enabled, applying bytecode optimization (-OO) and stripping dev modules...")) - if not self.params.get('use_venv'): + if not self.params.get("use_venv"): self.progress.emit(_("[WARN] Strongly recommend checking [Virtual Environment] to maximize lite mode effect.")) - + if engine == "PyInstaller": cmd.append("--optimize=2") - safe_dev_excludes = ['unittest', 'doctest', 'pdb', 'pydoc', 'test', 'pytest', 'IPython', 'binder', 'tkinter.test'] + safe_dev_excludes = ["unittest", "doctest", "pdb", "pydoc", "test", "pytest", "IPython", "binder", "tkinter.test"] for ex in safe_dev_excludes: cmd.append(f"--exclude-module={ex}") elif engine == "Nuitka": cmd.append("--python-flag=-OO") - nuitka_dev_excludes = ['binder', 'tkinter.test', 'pip', 'wheel', 'distutils', 'pkg_resources', 'jupyter', 'notebook', 'IPython'] + cmd.append("--lto=yes") + + nuitka_dev_excludes = [ + "binder", + "tkinter.test", + "pip", + "wheel", + "distutils", + "pkg_resources", + "jupyter", + "notebook", + "IPython", + "pytest", + "_pytest", + "unittest", + "setuptools", + "pydoc", + "doctest", + "test", + "tests", + ] + + if any(q in imports_lower for q in ("pyside6", "pyqt6", "pyside2", "pyqt5")): + qt_heavy_modules = [ + "QtWebEngine", + "QtWebEngineWidgets", + "QtWebEngineCore", + "Qt3DCore", + "Qt3DRender", + "Qt3DInput", + "Qt3DAnimation", + "QtQuick", + "QtQml", + "QtVirtualKeyboard", + "QtDesigner", + "QtSql", + "QtTest", + "QtSensors", + "QtPositioning", + "QtLocation", + ] + for mod in qt_heavy_modules: + if mod.lower() not in imports_lower: + for prefix in ("PySide6", "PyQt6", "PySide2", "PyQt5"): + cmd.append(f"--nofollow-import-to={prefix}.{mod}") + for ex in nuitka_dev_excludes: cmd.append(f"--nofollow-import-to={ex}") @@ -5538,9 +6426,17 @@ class PackingThread(QThread): if not success and not self._is_cancelled: log_text = "\n".join(self.all_raw_logs) if any(kw in log_text for kw in ["NoSpaceLeft", "No space left on device", "[Errno 28]"]): - self.progress.emit(_("[ERROR] Build aborted: Insufficient disk space (NoSpaceLeft / Errno 28). Please clean up drive space (at least 5 GB free space recommended) and try again.")) + self.progress.emit( + _( + "[ERROR] Build aborted: Insufficient disk space (NoSpaceLeft / Errno 28). Please clean up drive space (at least 5 GB free space recommended) and try again." + ) + ) elif any(kw in log_text for kw in ["Allocation error", "not enough memory", "ZstdError", "out of memory"]): - self.progress.emit(_("[WARN] Memory allocation exception caught (ZstdError / OOM). Triggering memory protection fallback: Retrying in single-thread mode...")) + self.progress.emit( + _( + "[WARN] Memory allocation exception caught (ZstdError / OOM). Triggering memory protection fallback: Retrying in single-thread mode..." + ) + ) clean_cmd = [arg if not arg.startswith("--jobs=") else "--jobs=1" for arg in cmd] if "--low-memory" not in clean_cmd: clean_cmd.append("--low-memory") @@ -5549,11 +6445,19 @@ class PackingThread(QThread): if not success and icon_path and not self._is_cancelled: log_text = "\n".join(self.all_raw_logs) - icon_err_keywords = ["Failed to add resources", "error code 22", "UpdateResource", "Resource modification failed", "Failed to add"] + icon_err_keywords = [ + "Failed to add resources", + "error code 22", + "UpdateResource", + "Resource modification failed", + "Failed to add", + ] if any(kw in log_text for kw in icon_err_keywords): - self.progress.emit(_("[WARN] Icon resource writing blocked (possibly locked by system/antivirus), triggering fallback protection...")) + self.progress.emit( + _("[WARN] Icon resource writing blocked (possibly locked by system/antivirus), triggering fallback protection...") + ) self.progress.emit(_("[INFO] Stripping icon parameters and automatically rebuilding...")) - + clean_cmd = [] skip_next = False for arg in cmd: @@ -5568,7 +6472,7 @@ class PackingThread(QThread): if icon_path in arg and "--include-data-files=" in arg: continue clean_cmd.append(arg) - + self.all_raw_logs.clear() success = self.run_cmd(clean_cmd, cwd=script_dir.as_posix()) @@ -5576,38 +6480,41 @@ class PackingThread(QThread): return self.build_finished.emit(False, _("[INFO] Build Cancelled."), []) src_out = None - if engine == "PyInstaller": - if sys.platform == "darwin" and self.params['noconsole']: + if engine == "PyInstaller": + if sys.platform == "darwin" and self.params["noconsole"]: src_out = self.temp_dist_dir / f"{app_name}.app" else: - src_out = self.temp_dist_dir / (f"{app_name}{ext}" if self.params['onefile'] else app_name) - elif engine == "Nuitka": - if self.params['onefile']: - if sys.platform == "darwin" and self.params['noconsole']: + src_out = self.temp_dist_dir / (f"{app_name}{ext}" if self.params["onefile"] else app_name) + elif engine == "Nuitka": + if self.params["onefile"]: + if sys.platform == "darwin" and self.params["noconsole"]: src_out = self.temp_out_dir / f"{app_name}.app" else: src_out = self.temp_out_dir / f"{app_name}{ext}" else: - if sys.platform == "darwin" and self.params['noconsole']: + if sys.platform == "darwin" and self.params["noconsole"]: src_out = self.temp_out_dir / f"{app_name}.app" else: dist_dirs = list(self.temp_out_dir.glob("*.dist")) - if dist_dirs: src_out = dist_dirs[0] - else: src_out = self.temp_out_dir / f"{app_name}.dist" + if dist_dirs: + src_out = dist_dirs[0] + else: + src_out = self.temp_out_dir / f"{app_name}.dist" - out_mode = int(self.params.get('out_mode', 0) or 0) - custom_out = (self.params.get('custom_out_dir') or '').strip() + out_mode = int(self.params.get("out_mode", 0) or 0) + custom_out = (self.params.get("custom_out_dir") or "").strip() if out_mode == 1 and custom_out: try: final_out_dir = Path(custom_out) final_out_dir.mkdir(parents=True, exist_ok=True) - except: final_out_dir = script_dir + except Exception: + final_out_dir = script_dir else: final_out_dir = script_dir - - if sys.platform == "darwin" and self.params['noconsole']: + + if sys.platform == "darwin" and self.params["noconsole"]: final_out = final_out_dir / f"{app_name}.app" - elif self.params['onefile']: + elif self.params["onefile"]: final_out = final_out_dir / f"{app_name}{ext}" else: final_out = final_out_dir / app_name @@ -5626,7 +6533,7 @@ class PackingThread(QThread): except PermissionError: time.sleep(1) final_out.unlink(missing_ok=True) - + for _retry in range(15): try: shutil.move(src_out.as_posix(), final_out.as_posix()) @@ -5637,38 +6544,59 @@ class PackingThread(QThread): raise PermissionError("File locked by Antivirus or other process.") except PermissionError: success = False - self.progress.emit(_("[ERROR] Target file is running or occupied. Please close the existing application and try again.")) - except Exception as e: + self.progress.emit( + _("[ERROR] Target file is running or occupied. Please close the existing application and try again.") + ) + except Exception as e: success = False - self.progress.emit(_("[ERROR] Product transfer failed, file might be occupied by system process or lack permission: {error}", error=str(e))) + self.progress.emit( + _( + "[ERROR] Product transfer failed, file might be occupied by system process or lack permission: {error}", + error=str(e), + ) + ) if src_out and src_out.exists(): - self.progress.emit(f"[INFO] You can manually copy the compiled product from temporary folder:\n -> {src_out.as_posix()}") - else: + self.progress.emit( + f"[INFO] You can manually copy the compiled product from temporary folder:\n -> {src_out.as_posix()}" + ) + else: if not success and not self._is_cancelled: - self.progress.emit(_("[ERROR] Could not locate valid executable product in temporary build directory: {path}", path=str(src_out))) + self.progress.emit( + _("[ERROR] Could not locate valid executable product in temporary build directory: {path}", path=str(src_out)) + ) - if success and final_out.exists(): - if self.params.get('enable_sign'): + if success and final_out.exists(): + if self.params.get("enable_sign"): self.auto_sign_executable(final_out) self.progress.emit(_("[INFO] Validating output files and generating final product...")) - if self.params.get('auto_save_log') and self.all_raw_logs: + if self.params.get("auto_save_log") and self.all_raw_logs: try: log_file = final_out_dir / f"qpypack_build_{app_name}.log" - log_file.write_text('\n'.join(self.all_raw_logs), encoding='utf-8') + log_file.write_text("\n".join(self.all_raw_logs), encoding="utf-8") self.progress.emit(_("[INFO] Build log exported to: {path}", path=log_file.as_posix())) - except: pass - - if self.params['onefile'] and os.name == 'nt': - self.progress.emit(_("[INFO] Tip: Executables without commercial signatures might be falsely flagged by Windows Defender/Antivirus.")) - self.build_finished.emit(True, _("[SUCCESS] Compilation completed, output path: {path}", path=final_out.resolve().as_posix()), failed_packages) - else: + except Exception: + pass + + if self.params["onefile"] and os.name == "nt": + self.progress.emit( + _("[INFO] Tip: Executables without commercial signatures might be falsely flagged by Windows Defender/Antivirus.") + ) + self.build_finished.emit( + True, _("[SUCCESS] Compilation completed, output path: {path}", path=final_out.resolve().as_posix()), failed_packages + ) + else: if self._is_cancelled: return self.build_finished.emit(False, _("[INFO] Build Cancelled."), []) err_info = self.detect_python_syntax_errors() if err_info["is_code_error"]: - msg = _("[Syntax Error] Source code contains syntax errors, compilation aborted:\n - File: {file}\n - Type: {type}\n - Line: Line {line}\n - Detail: {desc}\n\nTip: Please ensure the source code runs locally before packaging.", - file=err_info['file'], type=err_info['type'], line=err_info['line'], desc=err_info['desc']) + msg = _( + "[Syntax Error] Source code contains syntax errors, compilation aborted:\n - File: {file}\n - Type: {type}\n - Line: Line {line}\n - Detail: {desc}\n\nTip: Please ensure the source code runs locally before packaging.", + file=err_info["file"], + type=err_info["type"], + line=err_info["line"], + desc=err_info["desc"], + ) else: msg = _( "[FAILED] Build interrupted exceptionally!\n\n" @@ -5685,10 +6613,10 @@ class PackingThread(QThread): return self.build_finished.emit(False, _("[INFO] Build Cancelled."), []) err_trace = traceback.format_exc() self.progress.emit(f"[DETAILED_ONLY]\n[FATAL ERROR TRACE]\n{err_trace}\n") - self.build_finished.emit(False, f"[ERROR] {str(e)}\n\n(See Detailed Log for complete trace)", failed_packages) + self.build_finished.emit(False, f"[ERROR] {e!s}\n\n(See Detailed Log for complete trace)", failed_packages) finally: cleanup_errors = [] - + if "PYTHONPATH" in os.environ and "_qpypack_temp_" in os.environ.get("PYTHONPATH", ""): try: parts = os.environ["PYTHONPATH"].split(os.pathsep) @@ -5697,72 +6625,71 @@ class PackingThread(QThread): os.environ.pop("PYTHONPATH", None) except Exception as e: logger.debug(f"Failed to clean PYTHONPATH: {e}") - + if is_temp and build_script_path and build_script_path.exists(): - try: + try: build_script_path.unlink() pycache_dir = script_dir / "__pycache__" - if pycache_dir.exists(): + if pycache_dir.exists(): if not robust_rmtree(pycache_dir): cleanup_errors.append(f"__pycache__: {pycache_dir}") except Exception as e: logger.debug(f"Failed to remove temp script: {e}") cleanup_errors.append(f"temp script: {build_script_path}") - - if self.params.get('version_file'): + + if self.params.get("version_file"): try: - p = Path(self.params['version_file']) - if p.exists(): p.unlink() + p = Path(self.params["version_file"]) + if p.exists(): + p.unlink() except Exception as e: logger.debug(f"Failed to remove version file: {e}") - - if self.params.get('temp_icon_file'): + + if self.params.get("temp_icon_file"): try: - p = Path(self.params['temp_icon_file']) - if p.exists(): p.unlink() + p = Path(self.params["temp_icon_file"]) + if p.exists(): + p.unlink() except Exception as e: logger.debug(f"Failed to remove temp icon: {e}") - - if self.params['clean_all']: + + if self.params["clean_all"]: self.progress.emit(_("[INFO] Freeing up space, cleaning temporary build environment...")) - cleanup_dirs = [ - (self.temp_workpath, "workpath"), - (self.temp_out_dir, "output"), - (self.temp_dist_dir, "dist") - ] - - if not self.params.get('keep_venv'): + cleanup_dirs = [(self.temp_workpath, "workpath"), (self.temp_out_dir, "output"), (self.temp_dist_dir, "dist")] + + if not self.params.get("keep_venv"): cleanup_dirs.append((self.venv_dir, "venv")) - + for dir_path, dir_name in cleanup_dirs: if dir_path and dir_path.exists(): if not robust_rmtree(dir_path): cleanup_errors.append(f"{dir_name}: {dir_path}") - - app_name = self.params.get('app_name', 'app') + + app_name = self.params.get("app_name", "app") for p in ["__pycache__", f"{app_name}.build", f"{app_name}.onefile-build"]: dir_to_remove = script_dir / p if dir_to_remove.exists(): if not robust_rmtree(dir_to_remove): cleanup_errors.append(f"build dir: {dir_to_remove}") - + spec_file = script_dir / f"{app_name}.spec" if spec_file.exists(): - try: + try: spec_file.unlink() except Exception as e: logger.debug(f"Failed to remove spec file: {e}") - + if sandbox_mode == 0 and custom_temp_base and custom_temp_base.exists(): try: if not any(custom_temp_base.iterdir()): custom_temp_base.rmdir() except Exception as e: logger.debug(f"Failed to remove temp base: {e}") - + if cleanup_errors: logger.warning(f"Failed to cleanup: {', '.join(cleanup_errors)}") + class PythonInstallMonitorThread(QThread): finished_signal = Signal(bool) @@ -5774,14 +6701,15 @@ class PythonInstallMonitorThread(QThread): try: cmd = [self.exe_path, "/passive", "PrependPath=1", "Include_pip=1", "SimpleInstall=1"] kwargs = {} - if os.name == 'nt': + if os.name == "nt": kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW - + res = subprocess.run(cmd, **kwargs) self.finished_signal.emit(res.returncode == 0) except Exception: self.finished_signal.emit(False) + class PythonInstallerDialog(QDialog): def __init__(self, parent=None, is_missing_mode=True): super().__init__(parent) @@ -5822,9 +6750,9 @@ class PythonInstallerDialog(QDialog): layout.addWidget(lbl_title) if is_missing_mode: - tip_card = QLabel(_( - "Recommendation: Python 3.11.9 is recommended for optimal build compatibility and engine support." - )) + tip_card = QLabel( + _("Recommendation: Python 3.11.9 is recommended for optimal build compatibility and engine support.") + ) tip_card.setWordWrap(True) tip_card.setStyleSheet(""" QLabel { @@ -5860,7 +6788,9 @@ class PythonInstallerDialog(QDialog): lay_local.addWidget(self.local_list) self.btn_use_local = QPushButton(_("Use Selected Environment")) - self.btn_use_local.setStyleSheet("QPushButton { background-color: #16a34a; color: white; border: none; border-radius: 6px; font-size: 13px; font-weight: bold; padding: 8px 16px; } QPushButton:hover { background-color: #15803d; }") + self.btn_use_local.setStyleSheet( + "QPushButton { background-color: #16a34a; color: white; border: none; border-radius: 6px; font-size: 13px; font-weight: bold; padding: 8px 16px; } QPushButton:hover { background-color: #15803d; }" + ) self.btn_use_local.clicked.connect(self._on_use_local_clicked) lay_local.addWidget(self.btn_use_local) @@ -5907,13 +6837,17 @@ class PythonInstallerDialog(QDialog): btn_lay = QHBoxLayout() self.btn_manual_config = QPushButton(_("Configure Manually")) - self.btn_manual_config.setStyleSheet("QPushButton { background-color: #f8fafc; color: #2563eb; border: 1px solid #bfdbfe; border-radius: 6px; font-size: 12px; font-weight: bold; padding: 6px 14px; } QPushButton:hover { background-color: #eff6ff; }") + self.btn_manual_config.setStyleSheet( + "QPushButton { background-color: #f8fafc; color: #2563eb; border: 1px solid #bfdbfe; border-radius: 6px; font-size: 12px; font-weight: bold; padding: 6px 14px; } QPushButton:hover { background-color: #eff6ff; }" + ) self.btn_manual_config.clicked.connect(lambda: self.done(2)) btn_lay.addWidget(self.btn_manual_config) btn_lay.addStretch() self.btn_cancel = QPushButton(_("Cancel")) - self.btn_cancel.setStyleSheet("QPushButton { background-color: #f1f5f9; color: #475569; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 12px; font-weight: bold; padding: 6px 14px; } QPushButton:hover { background-color: #e2e8f0; }") + self.btn_cancel.setStyleSheet( + "QPushButton { background-color: #f1f5f9; color: #475569; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 12px; font-weight: bold; padding: 6px 14px; } QPushButton:hover { background-color: #e2e8f0; }" + ) self.btn_cancel.clicked.connect(self.reject) btn_lay.addWidget(self.btn_cancel) layout.addLayout(btn_lay) @@ -5930,12 +6864,12 @@ class PythonInstallerDialog(QDialog): def _scan_installed_map(self): installed = {} - if self.parent_win and hasattr(self.parent_win, 'settings_panel'): + if self.parent_win and hasattr(self.parent_win, "settings_panel"): combo = self.parent_win.settings_panel.python_path_combo for i in range(combo.count()): text = combo.itemText(i) path = combo.itemData(i) or text - m = re.search(r'Python\s+(\d+\.\d+)', text, re.I) + m = re.search(r"Python\s+(\d+\.\d+)", text, re.IGNORECASE) if m and path and os.path.exists(path): installed[m.group(1)] = path @@ -5948,7 +6882,7 @@ class PythonInstallerDialog(QDialog): def _on_download_ver_changed(self): ver_full = self.dl_combo.currentData() - parts = ver_full.split('.') + parts = ver_full.split(".") major_minor = f"{parts[0]}.{parts[1]}" if len(parts) >= 2 else ver_full local_path = self.installed_map.get(major_minor) or self.installed_map.get(ver_full) @@ -5956,12 +6890,14 @@ class PythonInstallerDialog(QDialog): if local_path: self.is_selected_ver_local = True self.current_local_target_path = local_path - self.lbl_dl_hint.setText(_( - "✔ Detected locally:
" - "{path}
" - "Ready to switch directly without re-downloading.", - path=local_path - )) + self.lbl_dl_hint.setText( + _( + " Detected locally:
" + "{path}
" + "Ready to switch directly without re-downloading.", + path=local_path, + ) + ) self.btn_dl_install.setText(_("Use Installed Version Directly")) self.btn_dl_install.setStyleSheet(""" QPushButton { background-color: #16a34a; color: white; border: none; border-radius: 6px; font-size: 13px; font-weight: bold; padding: 10px 16px; } @@ -5987,7 +6923,7 @@ class PythonInstallerDialog(QDialog): def _load_local_environments(self): self.local_list.clear() found_any = False - if self.parent_win and hasattr(self.parent_win, 'settings_panel'): + if self.parent_win and hasattr(self.parent_win, "settings_panel"): combo = self.parent_win.settings_panel.python_path_combo for i in range(combo.count()): text = combo.itemText(i) @@ -6024,7 +6960,8 @@ class PythonInstallerDialog(QDialog): return self.dl_combo.currentData() def get_selected_local_path(self): - return getattr(self, 'selected_local_path', None) + return getattr(self, "selected_local_path", None) + class MainWindow(QMainWindow): def __init__(self): @@ -6032,38 +6969,38 @@ class MainWindow(QMainWindow): self.script_path = "" self.thread = None self.analysis_thread = None - self.current_state = "idle" - + self.current_state = "idle" + load_config() - + self.init_style() self.init_ui() - + I18N.language_changed.connect(self.retranslate_ui) self.retranslate_ui() def init_style(self): self.setWindowTitle(f"{__app_name__} {__version__}") self.setMinimumSize(560, 460) - + try: config = load_config() - w = int(config.get('Settings', 'window_width', fallback='760')) - h = int(config.get('Settings', 'window_height', fallback='580')) + w = int(config.get("Settings", "window_width", fallback="760")) + h = int(config.get("Settings", "window_height", fallback="580")) self.resize(w, h) - if config.get('Settings', 'window_maximized', fallback='False') == 'True': + if config.get("Settings", "window_maximized", fallback="False") == "True": self.setWindowState(Qt.WindowState.WindowMaximized) except Exception: self.resize(760, 580) - + icon_path = get_resource_path("icon.ico") if os.path.exists(icon_path): self.setWindowIcon(QIcon(icon_path)) - elif getattr(sys, 'frozen', False): - + elif getattr(sys, "frozen", False): provider = QFileIconProvider() exe_icon = provider.icon(QFileInfo(sys.executable)) - if not exe_icon.isNull(): self.setWindowIcon(exe_icon) + if not exe_icon.isNull(): + self.setWindowIcon(exe_icon) self.setStyleSheet(""" QMainWindow { background-color: #ffffff; } @@ -6071,7 +7008,7 @@ class MainWindow(QMainWindow): QStatusBar { background-color: #f8f9fa; color: #5f6368; border-top: 1px solid #e8eaed; padding: 5px; } QStatusBar QLabel { color: #5f6368; font-size: 13px; padding: 2px; background: transparent; } """) - + self.icon_btn_style = "QPushButton { background-color: #f1f3f4; border: 1px solid transparent; border-radius: 8px; } QPushButton:hover { background-color: #e8eaed; } QPushButton:pressed { background-color: #dadce0; }" self.primary_btn_style = "QPushButton { background-color: #1A73E8; color: white; border: none; border-radius: 8px; font-size: 15px; font-weight: bold; } QPushButton:hover { background-color: #1B66C9; } QPushButton:pressed { background-color: #174EA6; }" self.danger_btn_style = "QPushButton { background-color: #D93025; color: white; border: none; border-radius: 8px; font-size: 15px; font-weight: bold; } QPushButton:hover { background-color: #C5221F; } QPushButton:pressed { background-color: #A50E0E; }" @@ -6094,12 +7031,12 @@ class MainWindow(QMainWindow): self.log_container = QWidget() log_lay = QVBoxLayout(self.log_container) log_lay.setContentsMargins(0, 0, 0, 0) - + log_header = QHBoxLayout() log_header.setContentsMargins(5, 0, 5, 2) self.log_title = QLabel(_("Execution Log")) self.log_title.setStyleSheet("color: #5f6368; font-weight: bold; font-size: 13px;") - + self.btn_toggle_log_mode = QPushButton() self.btn_toggle_log_mode.setCheckable(True) self.btn_toggle_log_mode.setCursor(Qt.CursorShape.PointingHandCursor) @@ -6108,18 +7045,18 @@ class MainWindow(QMainWindow): QPushButton:hover { color: #1B66C9; text-decoration: underline; } """) self.btn_toggle_log_mode.clicked.connect(self.on_log_mode_toggled) - + log_header.addWidget(self.log_title) log_header.addStretch(1) log_header.addWidget(self.btn_toggle_log_mode) - + self.log_stack_widget = QWidget() self.log_stack = QStackedLayout(self.log_stack_widget) self.log_stack.setContentsMargins(0, 0, 0, 0) - + self.log_concise = QTextEdit() self.log_detailed = QTextEdit() - + for text_edit in (self.log_concise, self.log_detailed): text_edit.setReadOnly(True) text_edit.setMinimumHeight(80) @@ -6127,7 +7064,7 @@ class MainWindow(QMainWindow): text_edit.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) text_edit.customContextMenuRequested.connect(self.show_log_context_menu) self.log_stack.addWidget(text_edit) - + log_lay.addLayout(log_header) log_lay.addWidget(self.log_stack_widget) self.log_container.hide() @@ -6151,7 +7088,7 @@ class MainWindow(QMainWindow): self.btn_right = AnimatedButton("") self.btn_right.setFixedSize(44, 44) - self.btn_right.setIcon(get_svg_icon('settings', "#5F6368")) + self.btn_right.setIcon(get_svg_icon("settings", "#5F6368")) self.btn_right.setStyleSheet(self.icon_btn_style) self.btn_right.clicked.connect(self.show_settings) btn_layout.addWidget(self.btn_right) @@ -6162,7 +7099,7 @@ class MainWindow(QMainWindow): self.settings_panel = SettingsPanel(self) self.stacked_layout.addWidget(self.settings_panel) self.stacked_layout.setCurrentWidget(self.main_panel) - + is_concise = self.settings_panel.concise_log_check.isChecked() self.btn_toggle_log_mode.setChecked(not is_concise) self.btn_toggle_log_mode.setText(_("Detailed Mode") if is_concise else _("Concise Mode")) @@ -6176,23 +7113,27 @@ class MainWindow(QMainWindow): year_str = f"2026-{current_year}" if current_year > 2026 else "2026" self.copyright_label = QLabel(f"Copyright © {year_str} {__author__}. ") - self.copyright_label.setStyleSheet("color: #bdc1c6; font-size: 11px; font-weight: bold; background: transparent; padding-right: 5px;") + self.copyright_label.setStyleSheet( + "color: #bdc1c6; font-size: 11px; font-weight: bold; background: transparent; padding-right: 5px;" + ) self.status_bar.addPermanentWidget(self.copyright_label) self.update_ui_state("idle") def show_notification(self, msg, timeout=4000): - msg = msg.replace('\n', ' ') + msg = msg.replace("\n", " ") self.statusBar().showMessage(msg, timeout) def _on_python_install_completed(self, success): if success: msg_box = QMessageBox(self) msg_box.setWindowTitle(_("Install Complete")) - msg_box.setText(_( - "Python Environment Installed Successfully!

" - "System environment variables updated. Recommended to exit and restart the software to apply changes." - )) + msg_box.setText( + _( + "Python Environment Installed Successfully!

" + "System environment variables updated. Recommended to exit and restart the software to apply changes." + ) + ) msg_box.setIcon(QMessageBox.Icon.Information) btn_exit = msg_box.addButton(_("Exit Now"), QMessageBox.ButtonRole.AcceptRole) @@ -6238,13 +7179,13 @@ class MainWindow(QMainWindow): self.adjust_status_bar() def adjust_status_bar(self): - if not hasattr(self, 'status_label') or not hasattr(self, 'copyright_label') or not hasattr(self, 'status_bar'): + if not hasattr(self, "status_label") or not hasattr(self, "copyright_label") or not hasattr(self, "status_bar"): return text = self.status_label.text() metrics = self.status_label.fontMetrics() text_width = metrics.horizontalAdvance(text) copyright_width = self.copyright_label.fontMetrics().horizontalAdvance(self.copyright_label.text()) - + if text_width + copyright_width + 60 > self.status_bar.width(): self.copyright_label.hide() else: @@ -6260,10 +7201,10 @@ class MainWindow(QMainWindow): self.btn_left.setToolTip(_("Reset to Default Config")) else: self.btn_left.setToolTip(_("Toggle Execution Log")) - + self.update_ui_state(self.current_state) - if hasattr(self, 'log_title'): + if hasattr(self, "log_title"): self.log_title.setText(_("Execution Log")) is_concise = self.settings_panel.concise_log_check.isChecked() self.btn_toggle_log_mode.setText(_("Detailed Mode") if is_concise else _("Concise Mode")) @@ -6272,21 +7213,21 @@ class MainWindow(QMainWindow): self.set_status(_("Status: Ready")) self.drop_area.label.setText(_("Python Packaging, Reimagined.")) self.drop_area.sub_label.setText(_("Drop Python source code to start")) - + elif self.current_state == "ready": mode_suffix = _(" [Console]") if not self.settings_panel.noconsole_check.isChecked() else _(" [No Console]") self.set_status(_("Status: Loaded {filename}{mode}", filename=Path(self.script_path).name, mode=mode_suffix)) self.drop_area.label.setText(_("Loaded: {filename}", filename=Path(self.script_path).name)) self.drop_area.sub_label.setText(_("Ready, waiting for build.")) - + elif self.current_state == "building": self.set_status(_("Status: Packaging ({engine}) ...", engine=self.settings_panel.engine_combo.currentText())) - + elif self.current_state == "done": self.set_status(_("Status: Build Completed")) self.drop_area.label.setText(_("Build Successful")) self.drop_area.sub_label.setText(_("Open output directory or reset workspace.")) - + elif self.current_state == "failed": self.set_status(_("Status: Build Failed")) self.drop_area.label.setText(_("Build Failed")) @@ -6299,11 +7240,12 @@ class MainWindow(QMainWindow): def closeEvent(self, event): try: config = load_config() - if 'Settings' not in config: config['Settings'] = {} + if "Settings" not in config: + config["Settings"] = {} if not self.isMaximized() and not self.isFullScreen(): - config['Settings']['window_width'] = str(self.width()) - config['Settings']['window_height'] = str(self.height()) - config['Settings']['window_maximized'] = str(self.isMaximized()) + config["Settings"]["window_width"] = str(self.width()) + config["Settings"]["window_height"] = str(self.height()) + config["Settings"]["window_maximized"] = str(self.isMaximized()) save_config(config) except Exception: pass @@ -6311,69 +7253,75 @@ class MainWindow(QMainWindow): if self.thread and self.thread.isRunning(): self.thread.cancel() self.thread.wait(2000) - + if self.analysis_thread and self.analysis_thread.isRunning(): self.analysis_thread.requestInterruption() self.analysis_thread.wait(1000) - - if hasattr(self,'settings_panel') and hasattr(self.settings_panel, 'scanner_thread'): + + if hasattr(self, "settings_panel") and hasattr(self.settings_panel, "scanner_thread"): if self.settings_panel.scanner_thread and self.settings_panel.scanner_thread.isRunning(): self.settings_panel.scanner_thread.requestInterruption() self.settings_panel.scanner_thread.wait(1000) - + super().closeEvent(event) def update_ui_state(self, state): self.current_state = state self.btn_right.setEnabled(state != "building") self.drop_area.setAcceptDrops(state != "building") - + if state in ("idle", "ready"): is_log_open = self.log_container.isVisible() - icon_name = 'expand_less' if is_log_open else 'expand_more' + icon_name = "expand_less" if is_log_open else "expand_more" self.btn_left.setIcon(get_svg_icon(icon_name, "#5F6368")) - + self.btn_main.setText(_("Start Build")) - self.btn_main.setIcon(get_svg_icon('play', "white")) + self.btn_main.setIcon(get_svg_icon("play", "white")) self.btn_main.setStyleSheet(self.primary_btn_style) - + elif state == "building": is_log_open = self.log_container.isVisible() - icon_name = 'expand_less' if is_log_open else 'expand_more' + icon_name = "expand_less" if is_log_open else "expand_more" self.btn_left.setIcon(get_svg_icon(icon_name, "#5F6368")) - + self.btn_main.setText(_("Stop Build")) - self.btn_main.setIcon(get_svg_icon('stop', "white")) + self.btn_main.setIcon(get_svg_icon("stop", "white")) self.btn_main.setStyleSheet(self.danger_btn_style) - + elif state in ("done", "failed"): - self.btn_left.setIcon(get_svg_icon('refresh', "#5F6368")) + self.btn_left.setIcon(get_svg_icon("refresh", "#5F6368")) if state == "done": self.btn_main.setText(_("Open Directory")) - self.btn_main.setIcon(get_svg_icon('folder', "white")) + self.btn_main.setIcon(get_svg_icon("folder", "white")) self.btn_main.setStyleSheet(self.success_btn_style) else: self.btn_main.setText(_("Rebuild")) - self.btn_main.setIcon(get_svg_icon('refresh', "white")) + self.btn_main.setIcon(get_svg_icon("refresh", "white")) self.btn_main.setStyleSheet(self.danger_btn_style) def on_left_btn_clicked(self): - if self.current_state in ("done", "failed"): + if self.current_state in ("done", "failed"): self.reset_to_ready() - else: + else: self.toggle_log() def on_main_btn_clicked(self): - if self.current_state in ("idle", "ready", "failed"): self.start_pack() - elif self.current_state == "building": self.cancel_pack() - elif self.current_state == "done": self.open_dist() + if self.current_state in ("idle", "ready", "failed"): + self.start_pack() + elif self.current_state == "building": + self.cancel_pack() + elif self.current_state == "done": + self.open_dist() def toggle_log(self): - if self.log_container.isVisible(): self.log_container.hide() - else: self.log_container.show() + if self.log_container.isVisible(): + self.log_container.hide() + else: + self.log_container.show() self.update_ui_state(self.current_state) - def show_settings(self): self._animate_switch(self.settings_panel) + def show_settings(self): + self._animate_switch(self.settings_panel) def show_main(self): self.settings_panel.load_from_config() @@ -6392,12 +7340,14 @@ class MainWindow(QMainWindow): if current_widget == target_widget: return - if hasattr(self, 'anim_group') and self.anim_group.state() == QParallelAnimationGroup.State.Running: + if hasattr(self, "anim_group") and self.anim_group.state() == QParallelAnimationGroup.State.Running: self.anim_group.stop() - for attr in ('lbl_old', 'lbl_new'): + for attr in ("lbl_old", "lbl_new"): if hasattr(self, attr) and getattr(self, attr): - try: getattr(self, attr).deleteLater() - except RuntimeError: pass + try: + getattr(self, attr).deleteLater() + except RuntimeError: + pass pix_old = current_widget.grab() @@ -6424,7 +7374,7 @@ class MainWindow(QMainWindow): self.lbl_new.setGeometry(x + w * direction, y, w, h) self.anim_group = QParallelAnimationGroup(self) - + anim_old = QPropertyAnimation(self.lbl_old, b"pos") anim_old.setDuration(280) anim_old.setEndValue(QPointF(x - w * direction, y)) @@ -6439,8 +7389,10 @@ class MainWindow(QMainWindow): self.anim_group.addAnimation(anim_new) def cleanup(): - if hasattr(self, 'lbl_old') and self.lbl_old: self.lbl_old.deleteLater() - if hasattr(self, 'lbl_new') and self.lbl_new: self.lbl_new.deleteLater() + if hasattr(self, "lbl_old") and self.lbl_old: + self.lbl_old.deleteLater() + if hasattr(self, "lbl_new") and self.lbl_new: + self.lbl_new.deleteLater() self.anim_group.finished.connect(cleanup) self.anim_group.start() @@ -6466,19 +7418,20 @@ class MainWindow(QMainWindow): def _auto_install_python(self, version): import urllib.request from urllib.error import URLError + from PySide6.QtWidgets import QProgressDialog - + url_hw = f"https://repo.huaweicloud.com/python/{version}/python-{version}-amd64.exe" url_off = f"https://www.python.org/ftp/python/{version}/python-{version}-amd64.exe" - + exe_path = Path(tempfile.gettempdir()) / f"python-{version}-amd64.exe" - + progress = QProgressDialog(_("Downloading Python {ver}... Please wait.", ver=version), _("Cancel"), 0, 100, self) progress.setWindowTitle(_("One-Click Install")) progress.setWindowModality(Qt.WindowModality.WindowModal) progress.setMinimumDuration(0) progress.setValue(0) - + def report(block_num, block_size, total_size): if total_size > 0: percent = int(block_num * block_size * 100 / total_size) @@ -6486,14 +7439,20 @@ class MainWindow(QMainWindow): QApplication.processEvents() if progress.wasCanceled(): raise Exception("Cancelled") - + try: target_url = url_hw if I18N.current_lang == "zh_CN" else url_off + + if not target_url.startswith(("https://", "http://")): + raise ValueError(f"Insecure download URL: {target_url}") + try: - urllib.request.urlretrieve(target_url, exe_path.as_posix(), reporthook=report) + urllib.request.urlretrieve(target_url, exe_path.as_posix(), reporthook=report) # nosec B310 except URLError: progress.setValue(0) - urllib.request.urlretrieve(url_off, exe_path.as_posix(), reporthook=report) + if not url_off.startswith(("https://", "http://")): + raise ValueError(f"Insecure download URL: {url_off}") from None + urllib.request.urlretrieve(url_off, exe_path.as_posix(), reporthook=report) # nosec B310 progress.setValue(100) self.show_notification(_("Download complete. Starting Python installation..."), 5000) @@ -6510,15 +7469,17 @@ class MainWindow(QMainWindow): if not self.script_path or not Path(self.script_path).exists(): self.show_error_log(_("[ERROR] Please load a valid Python source file first!")) return - + self.settings_panel.on_resources_dropped(paths) - + self.drop_area.icon_widget.trigger_drop_pop() - - add_data_items = [self.settings_panel.add_data_list.item(i).data(Qt.ItemDataRole.UserRole) - for i in range(self.settings_panel.add_data_list.count())] + + add_data_items = [ + self.settings_panel.add_data_list.item(i).data(Qt.ItemDataRole.UserRole) + for i in range(self.settings_panel.add_data_list.count()) + ] self.drop_area.render_asset_chips(self.script_path, add_data_items) - + count = len(paths) if count == 1: name = Path(paths[0]).name @@ -6546,50 +7507,71 @@ class MainWindow(QMainWindow): self.drop_area.set_loading(Path(path).name) self.drop_area.icon_widget.trigger_drop_pop() self.set_status(_("Status: Parsing {filename}...", filename=Path(path).name)) - + if self.analysis_thread and self.analysis_thread.isRunning(): self.analysis_thread.requestInterruption() self.analysis_thread.wait(1000) - + self.analysis_thread = ScriptAnalysisThread(self.script_path) self.analysis_thread.analysis_done.connect(self.on_analysis_finished) self.analysis_thread.start() def on_analysis_finished(self, app_name, version, author, desc, script_imports): path = self.script_path - if not path: return + if not path: + return + + if version: + self.settings_panel.ver_ver.setText(version) + else: + self.settings_panel.ver_ver.setText("1.0.0") - if version: self.settings_panel.ver_ver.setText(version) - else: self.settings_panel.ver_ver.setText("1.0.0") - self.settings_panel.ver_comp.setText(_(author) if author == "Independent Developer" else author) self.settings_panel.ver_desc.setText(_(desc) if desc == "Desktop Application" else desc) gui_libs = { - 'pyqt5', 'pyqt6', 'pyside2', 'pyside6', 'tkinter', 'wx', 'kivy', 'libavg', - 'pysimplegui', 'customtkinter', 'turtle', 'easygui', 'pygame', 'arcade', - 'dearpygui', 'flet', 'webview', 'remi', - 'qtpy', 'pyglet', 'toga', 'pyqtgraph' + "pyqt5", + "pyqt6", + "pyside2", + "pyside6", + "tkinter", + "wx", + "kivy", + "libavg", + "pysimplegui", + "customtkinter", + "turtle", + "easygui", + "pygame", + "arcade", + "dearpygui", + "flet", + "webview", + "remi", + "qtpy", + "pyglet", + "toga", + "pyqtgraph", } has_gui = any(lib in {m.lower() for m in script_imports} for lib in gui_libs) self.settings_panel.noconsole_check.setChecked(has_gui) script_dir = Path(path).parent - + found_venv_name = None try: import hashlib import re - + script_path_obj = Path(path).resolve() raw_stem = script_path_obj.stem - clean_stem = re.sub(r'[^a-zA-Z0-9]', '', raw_stem) or "script" + clean_stem = re.sub(r"[^a-zA-Z0-9]", "", raw_stem) or "script" short_stem = clean_stem[:12] - path_hash = hashlib.md5(script_path_obj.as_posix().encode('utf-8')).hexdigest()[:6] - + path_hash = hashlib.sha256(script_path_obj.as_posix().encode("utf-8"), usedforsecurity=False).hexdigest()[:6] + target_prefix = f".qpypack_venv_{short_stem}_{path_hash}" - + if script_dir.exists(): for p in script_dir.iterdir(): if p.is_dir() and p.name.startswith(target_prefix): @@ -6601,17 +7583,19 @@ class MainWindow(QMainWindow): if found_venv_name: self.settings_panel.venv_check.setChecked(True) self.settings_panel.keep_venv_check.setChecked(True) - self.append_log(_("[INFO] Auto-detected existing virtual environment '{venv}'. 'Keep Local Venv' is checked.", venv=found_venv_name)) + self.append_log( + _("[INFO] Auto-detected existing virtual environment '{venv}'. 'Keep Local Venv' is checked.", venv=found_venv_name) + ) else: self.settings_panel.keep_venv_check.setChecked(False) default_output_name = f"{app_name}_{version}" if version else app_name self.settings_panel.name_edit.setText(default_output_name) - + auto_icon = None if self.settings_panel.auto_icon_check.isChecked(): ext_priority = [".ico", ".png", ".jpg", ".jpeg", ".svg"] - if sys.platform == "darwin": + if sys.platform == "darwin": ext_priority = [".icns", ".png", ".svg", ".ico"] name_priority = ["logo", "icon", "app", "favicon", Path(path).stem] @@ -6624,18 +7608,23 @@ class MainWindow(QMainWindow): auto_icon = trial found = True break - if found: break - if found: break + if found: + break + if found: + break if auto_icon: self.settings_panel.icon_edit.setText(auto_icon.resolve().as_posix()) - + self.drop_area.set_success(Path(path).name, custom_icon_path=auto_icon) - add_data_items = [self.settings_panel.add_data_list.item(i).data(Qt.ItemDataRole.UserRole) for i in range(self.settings_panel.add_data_list.count())] + add_data_items = [ + self.settings_panel.add_data_list.item(i).data(Qt.ItemDataRole.UserRole) + for i in range(self.settings_panel.add_data_list.count()) + ] self.drop_area.render_asset_chips(self.script_path, add_data_items) mode_suffix = _(" [Console]") if not has_gui else _(" [No Console]") self.set_status(_("Status: Loaded {filename}{mode}", filename=Path(path).name, mode=mode_suffix)) - + self.log_concise.clear() self.log_detailed.clear() self.append_log(_("Loaded: {filename}", filename=path)) @@ -6647,31 +7636,31 @@ class MainWindow(QMainWindow): if self.thread and self.thread.isRunning(): self.thread.cancel() self.thread.wait(1000) - + self.append_log(_("[INFO] Build Cancelled.")) self.set_status(_("Status: Build Cancelled")) - + if self.script_path and Path(self.script_path).exists(): self.reset_to_ready() else: self.reset_all() - + self.show_notification(_("Build Cancelled")) def start_pack(self): if self.current_state == "building": return - + if not self.script_path or not Path(self.script_path).exists(): self.show_error_log(_("[ERROR] Please load a valid Python source file first!")) return sp = self.settings_panel - + raw_py = sp.python_path_combo.currentText().strip() - if " (Python " in raw_py: + if " (Python " in raw_py: raw_py = raw_py.split(" (Python ")[0].strip() - if not raw_py: + if not raw_py: raw_py = get_python_executable() if raw_py: @@ -6679,7 +7668,6 @@ class MainWindow(QMainWindow): if resolved_py: raw_py = Path(resolved_py).resolve().as_posix() - is_valid_py = False if raw_py: try: @@ -6687,8 +7675,9 @@ class MainWindow(QMainWindow): clean_env.pop("PYTHONHOME", None) clean_env.pop("PYTHONPATH", None) kwargs = {"stdout": subprocess.PIPE, "stderr": subprocess.PIPE, "text": True, "env": clean_env, "timeout": 3} - if os.name == 'nt': kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW - + if os.name == "nt": + kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW + proc = subprocess.run([raw_py, "-c", "import sys; print(sys.version_info.major)"], **kwargs) if proc.returncode == 0 and "3" in proc.stdout: is_valid_py = True @@ -6696,37 +7685,41 @@ class MainWindow(QMainWindow): pass if not is_valid_py: - if os.name == 'nt': + if os.name == "nt": self._trigger_python_download_dialog(is_missing_mode=True) else: msg = QMessageBox(self) msg.setIcon(QMessageBox.Icon.Warning) msg.setWindowTitle(_("Python Environment Required")) - msg.setText(_("Python is not detected on your system!

QPyPack requires a Python environment to compile your code.
If you haven't installed Python, please download and install it.")) + msg.setText( + _( + "Python is not detected on your system!

QPyPack requires a Python environment to compile your code.
If you haven't installed Python, please download and install it." + ) + ) btn_down = msg.addButton(_("Download Python"), QMessageBox.ButtonRole.ActionRole) msg.addButton(_("Cancel"), QMessageBox.ButtonRole.RejectRole) msg.exec() if msg.clickedButton() == btn_down: - __import__('webbrowser').open("https://www.python.org/downloads/") + __import__("webbrowser").open("https://www.python.org/downloads/") return validation_errors = [] if is_cloud_locked(self.script_path): validation_errors.append(_("Script file is locked by cloud sync")) - + icon_path_str = sp.icon_edit.text().strip() if icon_path_str and not Path(icon_path_str).exists(): validation_errors.append(_("Icon file not found: {path}", path=icon_path_str)) - + for i in range(sp.add_data_list.count()): item_data = sp.add_data_list.item(i).data(Qt.ItemDataRole.UserRole) if sp.add_data_list.item(i) else None if not item_data or len(item_data) != 3: continue - r_type, src, dst = item_data + _r_type, src, _dst = item_data if not Path(src).exists(): validation_errors.append(_("Resource not found: {path}", path=src)) - + if sp.out_mode_combo.currentIndex() == 1: custom_out = sp.out_dir_edit.text().strip() if custom_out: @@ -6738,9 +7731,9 @@ class MainWindow(QMainWindow): test_file.unlink() except Exception as e: validation_errors.append(_("Output directory not writable: {error}", error=str(e))) - + if validation_errors: - error_msg = "[ERROR] Configuration validation failed:\n" + "\n".join(f" • {err}" for err in validation_errors) + error_msg = "[ERROR] Configuration validation failed:\n" + "\n".join(f" {err}" for err in validation_errors) self.show_error_log(error_msg) return @@ -6751,50 +7744,47 @@ class MainWindow(QMainWindow): if engine == "PyInstaller" and os.name == "nt" and sp.ver_ver.text().strip(): try: v_str = sp.ver_ver.text().strip() - v_nums = re.findall(r'\d+', v_str) - v_tuple = ",".join((v_nums + ['0', '0', '0', '0'])[:4]) - + v_nums = re.findall(r"\d+", v_str) + v_tuple = ",".join((v_nums + ["0", "0", "0", "0"])[:4]) + comp_escaped = sp.ver_comp.text().replace("\\", "\\\\").replace("'", "\\'") desc_escaped = sp.ver_desc.text().replace("\\", "\\\\").replace("'", "\\'") v_str_escaped = v_str.replace("\\", "\\\\").replace("'", "\\'") - + clean_product_name = app_name if v_str: suffix = f"_{v_str}" if clean_product_name.endswith(suffix): - clean_product_name = clean_product_name[:-len(suffix)] + clean_product_name = clean_product_name[: -len(suffix)] - app_name_escaped = clean_product_name.replace("\\", "\\\\").replace("'", "\\'") orig_filename_escaped = app_name.replace("\\", "\\\\").replace("'", "\\'") - - content = f'''VSVersionInfo(ffi=FixedFileInfo(filevers=({v_tuple}),prodvers=({v_tuple}),mask=0x3f,flags=0x0,OS=0x40004,fileType=0x1,subtype=0x0,date=(0,0)),kids=[StringFileInfo([StringTable('040904B0',[StringStruct('CompanyName','{comp_escaped}'),StringStruct('FileDescription','{desc_escaped}'),StringStruct('FileVersion','{v_str_escaped}'),StringStruct('ProductVersion','{v_str_escaped}'),StringStruct('ProductName','{app_name_escaped}'),StringStruct('InternalName','{app_name_escaped}'),StringStruct('OriginalFilename','{orig_filename_escaped}.exe')])]),VarFileInfo([VarStruct('Translation',[1033,1200])])])''' + + content = f"""VSVersionInfo(ffi=FixedFileInfo(filevers=({v_tuple}),prodvers=({v_tuple}),mask=0x3f,flags=0x0,OS=0x40004,fileType=0x1,subtype=0x0,date=(0,0)),kids=[StringFileInfo([StringTable('040904B0',[StringStruct('CompanyName','{comp_escaped}'),StringStruct('FileDescription','{desc_escaped}'),StringStruct('FileVersion','{v_str_escaped}'),StringStruct('ProductVersion','{v_str_escaped}'),StringStruct('ProductName','{app_name_escaped}'),StringStruct('InternalName','{app_name_escaped}'),StringStruct('OriginalFilename','{orig_filename_escaped}.exe')])]),VarFileInfo([VarStruct('Translation',[1033,1200])])])""" version_file = Path(tempfile.gettempdir()) / f"qpypack_{app_name}_version.txt" - version_file.write_text(content, encoding='utf-8') - except: pass + version_file.write_text(content, encoding="utf-8") + except Exception: + pass icon_path_str = sp.icon_edit.text().strip() - + if icon_path_str and not Path(icon_path_str).exists(): self.show_error_log(_("[ERROR] The specified icon file does not exist: {path}", path=icon_path_str)) return - + temp_icon_file = None if icon_path_str: - icon_path = Path(icon_path_str) needed_ext = "ico" if os.name == "nt" else ("icns" if sys.platform == "darwin" else "png") is_valid_native_icon = False if icon_path.suffix.lower() == f".{needed_ext}": try: - with open(icon_path, 'rb') as f: + with open(icon_path, "rb") as f: header = f.read(4) - if needed_ext == "ico" and header == b'\x00\x00\x01\x00': + if (needed_ext == "ico" and header == b"\x00\x00\x01\x00") or (needed_ext == "icns" and header == b"icns"): is_valid_native_icon = True - elif needed_ext == "icns" and header == b'icns': - is_valid_native_icon = True - except: + except Exception: is_valid_native_icon = False if is_valid_native_icon: @@ -6821,92 +7811,104 @@ class MainWindow(QMainWindow): item_v = sp.mapping_table.item(r, 1) k = item_k.text().strip() if item_k else "" v = item_v.text().strip() if item_v else "" - if k and v: mappings[k] = v + if k and v: + mappings[k] = v params = { - 'engine': engine, - 'python_exe': raw_py, - 'script_path': self.script_path, - 'enable_sign': sp.enable_sign_check.isChecked(), - 'cert_path': sp.cert_path_edit.text().strip(), - 'cert_pass': sp.cert_pass_edit.text().strip(), - 'app_name': app_name, - 'onefile': sp.rb_onefile.isChecked(), - 'contents_dir': sp.contents_dir_edit.text().strip() or '_internal', - 'noconsole': sp.noconsole_check.isChecked(), - 'icon': icon_path_str, - 'use_reqs': sp.reqs_check.isChecked(), - 'use_pipreqs': sp.pipreqs_check.isChecked(), - 'use_pipreqs_dir': sp.pipreqs_dir_check.isChecked(), - 'reqs_file': sp.reqs_file_edit.text().strip(), - 'hidden_imports': sp.hidden_edit.text(), - 'add_data_list': add_data_items, - 'upx': sp.upx_check.isChecked(), - 'upx_path': sp.upx_path_edit.text().strip(), - 'cpu_cores': sp.cores_spin.value(), - 'exclude_modules': sp.exclude_edit.text().strip(), - 'out_mode': sp.out_mode_combo.currentIndex(), - 'custom_out_dir': sp.out_dir_edit.text().strip(), - 'temp_sandbox_mode': sp.sandbox_mode_combo.currentIndex(), - 'use_venv': sp.venv_check.isChecked(), - 'keep_venv': sp.keep_venv_check.isChecked(), - 'venv_mode': 'shared' if sp.rb_venv_shared.isChecked() else 'isolated', - 'shared_venv_dir': sp.shared_venv_dir_edit.text().strip(), - 'clean_all': sp.clean_all_check.isChecked(), - 'version_file': version_file.as_posix() if version_file else None, - 'temp_icon_file': temp_icon_file, - 'ver_comp': sp.ver_comp.text(), - 'ver_desc': sp.ver_desc.text(), - 'ver_ver': sp.ver_ver.text(), - 'pip_index_url': main_pip, - 'pip_index_backup': backup_pip, - 'concise_log': sp.concise_log_check.isChecked(), - 'auto_save_log': sp.auto_save_log_check.isChecked(), - 'lite_mode': sp.rb_lite_mode.isChecked(), - 'pyi_version': sp.pyi_ver_edit.text().strip(), - 'nuitka_version': sp.nuitka_ver_edit.text().strip(), - 'mappings': mappings, - 'enable_backport_shield': sp.enable_shield_check.isChecked(), - 'backport_rules': sp.get_current_active_backport_rules(), + "engine": engine, + "python_exe": raw_py, + "script_path": self.script_path, + "enable_sign": sp.enable_sign_check.isChecked(), + "cert_path": sp.cert_path_edit.text().strip(), + "cert_pass": sp.cert_pass_edit.text().strip(), + "app_name": app_name, + "onefile": sp.rb_onefile.isChecked(), + "contents_dir": sp.contents_dir_edit.text().strip() or "_internal", + "noconsole": sp.noconsole_check.isChecked(), + "icon": icon_path_str, + "use_reqs": sp.reqs_check.isChecked(), + "use_pipreqs": sp.pipreqs_check.isChecked(), + "use_pipreqs_dir": sp.pipreqs_dir_check.isChecked(), + "reqs_file": sp.reqs_file_edit.text().strip(), + "hidden_imports": sp.hidden_edit.text(), + "add_data_list": add_data_items, + "upx": sp.upx_check.isChecked(), + "upx_path": sp.upx_path_edit.text().strip(), + "cpu_cores": sp.cores_spin.value(), + "exclude_modules": sp.exclude_edit.text().strip(), + "out_mode": sp.out_mode_combo.currentIndex(), + "custom_out_dir": sp.out_dir_edit.text().strip(), + "temp_sandbox_mode": sp.sandbox_mode_combo.currentIndex(), + "use_venv": sp.venv_check.isChecked(), + "keep_venv": sp.keep_venv_check.isChecked(), + "venv_mode": "shared" if sp.rb_venv_shared.isChecked() else "isolated", + "shared_venv_dir": sp.shared_venv_dir_edit.text().strip(), + "clean_all": sp.clean_all_check.isChecked(), + "version_file": version_file.as_posix() if version_file else None, + "temp_icon_file": temp_icon_file, + "ver_comp": sp.ver_comp.text(), + "ver_desc": sp.ver_desc.text(), + "ver_ver": sp.ver_ver.text(), + "pip_index_url": main_pip, + "pip_index_backup": backup_pip, + "concise_log": sp.concise_log_check.isChecked(), + "auto_save_log": sp.auto_save_log_check.isChecked(), + "lite_mode": sp.rb_lite_mode.isChecked(), + "pyi_version": sp.pyi_ver_edit.text().strip(), + "nuitka_version": sp.nuitka_ver_edit.text().strip(), + "mappings": mappings, + "enable_backport_shield": sp.enable_shield_check.isChecked(), + "backport_rules": sp.get_current_active_backport_rules(), } self.log_concise.clear() self.log_detailed.clear() - if not self.log_container.isVisible(): self.toggle_log() - + if not self.log_container.isVisible(): + self.toggle_log() + self.thread = PackingThread(params) self.thread.progress.connect(self.append_log) self.thread.build_finished.connect(self.on_pack_finished) self.thread.start() - + self.set_status(_("Status: Packaging ({engine}) ...", engine=engine)) self.update_ui_state("building") self.drop_area.start_build_anim() def on_pack_finished(self, success, msg, failed_pkgs=None): - if not success and not (self.thread and getattr(self.thread, '_is_cancelled', False)): + if not success and not (self.thread and getattr(self.thread, "_is_cancelled", False)): log_content = self.log_detailed.toPlainText() - + if "MSVC 14.3 or later is required" in log_content or "scons environment variable 'CC' is not set" in log_content: msg = ( - _("[Environment Error] Build failed: C++ compiler version is incompatible.") + "\n\n" + - _("Reason: Python 3.11+ requires Visual Studio 2022 (MSVC 14.3) with Windows SDK.") + "\n" + - _("Solution:\n1. Install Visual Studio 2022 (Check 'Desktop development with C++' and 'Windows SDK').\n2. Or switch the engine to [PyInstaller] in Build Settings.") + _("[Environment Error] Build failed: C++ compiler version is incompatible.") + + "\n\n" + + _("Reason: Python 3.11+ requires Visual Studio 2022 (MSVC 14.3) with Windows SDK.") + + "\n" + + _( + "Solution:\n1. Install Visual Studio 2022 (Check 'Desktop development with C++' and 'Windows SDK').\n2. Or switch the engine to [PyInstaller] in Build Settings." + ) ) elif "MinGW64 does not work with Python 3.13" in log_content or "ziglang" in log_content: msg = ( - _("[Environment Error] Python 3.13+ Compiler Requirement.") + "\n\n" + - _("Reason: MinGW64 is no longer supported on Python 3.13+. Please ensure Zig or VS 2022 is installed.") + "\n" + - _("Solution:\n1. In Build Settings, switch to [PyInstaller] engine (Recommended).\n2. Or install Visual Studio 2022 with C++ support.") + _("[Environment Error] Python 3.13+ Compiler Requirement.") + + "\n\n" + + _("Reason: MinGW64 is no longer supported on Python 3.13+. Please ensure Zig or VS 2022 is installed.") + + "\n" + + _( + "Solution:\n1. In Build Settings, switch to [PyInstaller] engine (Recommended).\n2. Or install Visual Studio 2022 with C++ support." + ) ) - if not success and self.thread and self.thread.params.get('lite_mode') and not getattr(self.thread, '_is_cancelled', False): - msg += "\n\n" + _("Tip: Build failed in Lite Mode. You can try switching to [Compatibility Mode] in Build Settings and rebuild.") - + if not success and self.thread and self.thread.params.get("lite_mode") and not getattr(self.thread, "_is_cancelled", False): + msg += "\n\n" + _( + "Tip: Build failed in Lite Mode. You can try switching to [Compatibility Mode] in Build Settings and rebuild." + ) + self.append_log("\n" + msg) self.drop_area.stop_build_anim() - - if self.thread and getattr(self.thread, '_is_cancelled', False): + + if self.thread and getattr(self.thread, "_is_cancelled", False): if self.script_path and Path(self.script_path).exists(): self.reset_to_ready() else: @@ -6915,7 +7917,7 @@ class MainWindow(QMainWindow): self.set_status(_("Status: Build Cancelled")) return play_alert(success, self.settings_panel.sound_notify_check.isChecked()) - + if success: icon_path = self.settings_panel.icon_edit.text().strip() self.drop_area.show_success(icon_path) @@ -6926,10 +7928,13 @@ class MainWindow(QMainWindow): self.set_status(_("Status: Build Failed")) self.update_ui_state("failed") - if failed_pkgs and not (self.thread and getattr(self.thread, '_is_cancelled', False)): + if failed_pkgs and not (self.thread and getattr(self.thread, "_is_cancelled", False)): warn_msg = _("Dependency Missing Warning: {pkgs} failed to install. Check log for details.", pkgs=", ".join(failed_pkgs)) self.show_notification(warn_msg, 6000) - err_log = _("[ERROR] Build completed, but the following dependencies failed to install:\n\n - {pkgs}\n\nNote: The application might raise ModuleNotFoundError at runtime.", pkgs=", ".join(failed_pkgs)) + err_log = _( + "[ERROR] Build completed, but the following dependencies failed to install:\n\n - {pkgs}\n\nNote: The application might raise ModuleNotFoundError at runtime.", + pkgs=", ".join(failed_pkgs), + ) self.show_error_log(err_log) def open_dist(self): @@ -6937,11 +7942,12 @@ class MainWindow(QMainWindow): target = Path(self.settings_panel.out_dir_edit.text().strip()) else: target = Path(self.script_path).parent if self.script_path else Path.cwd() - + if target.exists(): try: QDesktopServices.openUrl(QUrl.fromLocalFile(target.resolve().as_posix())) - except: pass + except Exception: + pass def reset_to_ready(self): if self.script_path and Path(self.script_path).exists(): @@ -6965,18 +7971,19 @@ class MainWindow(QMainWindow): self.settings_panel.ver_ver.setText("1.0.0") self.settings_panel.ver_comp.setText(_("Independent Developer")) self.settings_panel.ver_desc.setText(_("Desktop Application")) - + self.settings_panel.keep_venv_check.setChecked(False) - + self.log_concise.clear() self.log_detailed.clear() - - if self.log_container.isVisible(): self.toggle_log() + + if self.log_container.isVisible(): + self.toggle_log() self.drop_area.reset() self.drop_area.render_asset_chips(None, None) self.set_status(_("Status: Workspace Reset")) self.update_ui_state("idle") - + def on_log_mode_toggled(self, checked): is_concise = not checked self.settings_panel.concise_log_check.setChecked(is_concise) @@ -6992,50 +7999,53 @@ class MainWindow(QMainWindow): logger.info(msg) else: logger.debug(msg) - + if msg.startswith("[DETAILED_ONLY]"): - clean_msg = msg.replace("[DETAILED_ONLY]", "", 1).lstrip('\r\n') + clean_msg = msg.replace("[DETAILED_ONLY]", "", 1).lstrip("\r\n") self._render_text_edit(self.log_detailed, clean_msg, is_error) return concise_lines = [] - - for line in msg.split('\n'): + + for line in msg.split("\n"): line_strip = line.strip() - if not line_strip: continue - + if not line_strip: + continue + if is_error: concise_lines.append(line) continue - + is_concise_kept = False - valid_prefixes = ( - "[INFO]", "[WARN]", "[SUCCESS]", "[FAILED]", "[ERROR]", "[Syntax Error]", - "•", "---", "━", "!" + valid_prefixes = ("[INFO]", "[WARN]", "[SUCCESS]", "[FAILED]", "[ERROR]", "[Syntax Error]", "•", "---", "━", "!") + + is_critical_engine_log = any( + kw in line for kw in ("FATAL:", "Disable Anti-Virus", "Failed to delete", "PermissionError", "Access is denied") ) - - is_critical_engine_log = any(kw in line for kw in ( - "FATAL:", "Disable Anti-Virus", "Failed to delete", "PermissionError", "Access is denied" - )) if any(line_strip.startswith(p) for p in valid_prefixes) or is_critical_engine_log: concise_lines.append(line) is_concise_kept = True - + if self.current_state == "building": if any(k in line for k in ("Nuitka", "Scons", "PyInstaller", "Compiling", "Building", "Linking")): - clean_sub = re.sub(r'^(Nuitka-Scons:|Nuitka:|INFO:\s*PyInstaller:|\d+\s+INFO:\s*)', '', line).strip() + clean_sub = re.sub(r"^(Nuitka-Scons:|Nuitka:|INFO:\s*PyInstaller:|\d+\s+INFO:\s*)", "", line).strip() if clean_sub and len(clean_sub) > 3 and not clean_sub.startswith("Used command line"): - if len(clean_sub) > 60: clean_sub = clean_sub[:57] + "..." + if len(clean_sub) > 60: + clean_sub = clean_sub[:57] + "..." self.drop_area.sub_label.setText(clean_sub) - + engine_name = self.settings_panel.engine_combo.currentText() - target_text = _("Status: Packaging ({engine}) ...", engine=engine_name).replace("Status: ", "").replace("状态: ", "") + target_text = ( + _("Status: Packaging ({engine}) ...", engine=engine_name).replace("Status: ", "").replace("状态: ", "") + ) if self.drop_area.label.text() != target_text: self.drop_area.label.setText(target_text) - self.drop_area.label.setStyleSheet("QLabel { background: transparent; color: #1A73E8; font-size: 16px; font-weight: bold; border: none; }") - + self.drop_area.label.setStyleSheet( + "QLabel { background: transparent; color: #1A73E8; font-size: 16px; font-weight: bold; border: none; }" + ) + if not is_concise_kept: concise_lines.append(f"[BUILD] {clean_sub}") @@ -7043,28 +8053,31 @@ class MainWindow(QMainWindow): clean_text = line_strip for prefix in ("[INFO]", "[WARN]", "[SUCCESS]", "[FAILED]", "[ERROR]"): if clean_text.startswith(prefix): - clean_text = clean_text[len(prefix):].strip() + clean_text = clean_text[len(prefix) :].strip() break - if len(clean_text) > 40: clean_text = clean_text[:37] + "..." + if len(clean_text) > 40: + clean_text = clean_text[:37] + "..." self.drop_area.label.setText(clean_text) - self.drop_area.label.setStyleSheet("QLabel { background: transparent; color: #1A73E8; font-size: 16px; font-weight: bold; border: none; }") + self.drop_area.label.setStyleSheet( + "QLabel { background: transparent; color: #1A73E8; font-size: 16px; font-weight: bold; border: none; }" + ) - concise_msg = '\n'.join(concise_lines) + concise_msg = "\n".join(concise_lines) if concise_msg: self._render_text_edit(self.log_concise, concise_msg, is_error) - + self._render_text_edit(self.log_detailed, msg, is_error) def _render_text_edit(self, text_edit, msg, is_error): if is_error: - safe_msg = msg.replace('<', '<').replace('>', '>').replace('\n', '
') + safe_msg = msg.replace("<", "<").replace(">", ">").replace("\n", "
") text_edit.append(f'{safe_msg}') else: text_edit.append(msg) text_edit.ensureCursorVisible() - def show_log_context_menu(self, pos): + def show_log_context_menu(self, pos): current_log = self.log_stack.currentWidget() menu = QMenu(self) menu.setStyleSheet(""" @@ -7098,7 +8111,7 @@ class MainWindow(QMainWindow): margin: 4px 6px; } """) - + act_copy = menu.addAction(_("Copy")) act_copy.setEnabled(current_log.textCursor().hasSelection()) act_copy.triggered.connect(current_log.copy) @@ -7120,22 +8133,29 @@ class MainWindow(QMainWindow): def save_log_file(self): current_log = self.log_stack.currentWidget() content = current_log.toPlainText() - + if not content.strip(): return self.show_notification(_("No log content.")) - + default_name = "build.log" if self.script_path: default_name = f"qpypack_{Path(self.script_path).stem}.log" - - fp, _filter = QFileDialog.getSaveFileName(self, _("Export Log..."), default_name, "Log Files (*.log);;Text Files (*.txt);;All Files (*)", options=QFileDialog.Option.DontUseNativeDialog) + + fp, _filter = QFileDialog.getSaveFileName( + self, + _("Export Log..."), + default_name, + "Log Files (*.log);;Text Files (*.txt);;All Files (*)", + options=QFileDialog.Option.DontUseNativeDialog, + ) if fp: try: - Path(fp).write_text(content, encoding='utf-8') + Path(fp).write_text(content, encoding="utf-8") self.show_notification(_("Log saved to: {path}", path=fp)) except Exception as e: self.show_error_log(_("[ERROR] Failed to export log file: {error}", error=str(e))) + def global_excepthook(exctype, value, tb): err_text = "".join(traceback.format_exception(exctype, value, tb)) logger.critical(f"Unhandled exception: {err_text}") @@ -7144,22 +8164,25 @@ def global_excepthook(exctype, value, tb): log_file = os.path.join(tempfile.gettempdir(), "qpypack_crash.log") with open(log_file, "a", encoding="utf-8") as f: f.write(err_text + "\n") - + from PySide6.QtWidgets import QApplication, QMessageBox + if QApplication.instance(): QMessageBox.critical( - None, - "Fatal Error / 严重错误", - f"App crashed! Unhandled exception occurred.\nLog saved to: {log_file}\n\n{err_text[:500]}..." + None, + "Fatal Error / 严重错误", + f"App crashed! Unhandled exception occurred.\nLog saved to: {log_file}\n\n{err_text[:500]}...", ) except Exception: pass + def main(): sys.excepthook = global_excepthook try: QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) - except Exception: pass + except Exception: + pass app = QApplication(sys.argv) app.setStyle("Fusion") @@ -7213,10 +8236,9 @@ def main(): I18N.update_qt_translator(I18N.current_lang) font = QFont() - font.setFamilies([ - "Segoe UI", "Microsoft YaHei", "PingFang SC", - "Hiragino Sans GB", "Noto Sans SC", "Helvetica Neue", "Arial", "sans-serif" - ]) + font.setFamilies( + ["Segoe UI", "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", "Helvetica Neue", "Arial", "sans-serif"] + ) font.setPointSize(9) app.setFont(font) @@ -7224,5 +8246,6 @@ def main(): win.show() sys.exit(app.exec()) + if __name__ == "__main__": main()