diff --git a/README.md b/README.md
index d1802a5..f5d6786 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,21 @@
# QPyPack
-QPyPack 是一个为 Python 开发者设计的图形化打包配置工具。它将复杂的命令行打包流程(支持 **PyInstaller**、**Nuitka** 和 **cx_Freeze**)转化为直观、简易的 GUI 界面操作,无需记忆繁琐的终端命令,即可快速生成可执行文件(.exe)。
+QPyPack 是一个为 Python 开发者设计的图形化打包配置工具。它将复杂的命令行打包流程转化为直观、简易的 GUI 界面操作,无需记忆繁琐的终端命令,即可快速生成可执行文件。
-
---
## 核心设计:让打包变简单
-为了解决传统命令行打包参数多、依赖易冲突、配置繁琐的问题,QPyPack 2.0 围绕“易用性”进行了重新设计:
+为了解决传统命令行打包参数多、依赖易冲突、配置繁琐的问题,QPyPack 围绕“易用性”与“官方最佳实践”进行了深度设计:
### 1. 极简的交互流程
* **拖拽即装载**:无需手动输入路径,直接将您的 `.py` 源代码文件拖入软件窗口,即可自动识别并准备构建。
* **智能图标匹配**:脚本载入后,程序会自动检索同目录下的常用图标文件(如 `icon.ico` / `logo.ico`)并自动填充和预览。
-* **一键直达结果**:打包完成后,点击“打开输出目录”按钮即可自动弹出 Windows 资源管理器并定位到生成的程序。
+* **一键直达结果**:打包完成后,点击“打开输出目录”按钮即可自动弹出资源管理器并定位到生成的程序。
### 2. 自动化的依赖与环境处理
* **智能沙盒隔离 (Venv)**:支持一键开启虚拟环境。工具会在临时目录下自动创建干净的沙盒环境,仅安装打包所需的库,有效减小最终生成的 `.exe` 文件的体积。
@@ -26,29 +25,43 @@ QPyPack 是一个为 Python 开发者设计的图形化打包配置工具。它
* **内置国内加速源**:预设清华大学等国内 PIP 镜像源,在沙盒中拉取打包工具或依赖库时自动加速。
### 3. 可视化高级参数配置
-* **三引擎无缝切换**:在下拉菜单中即可在 PyInstaller、Nuitka 和 cx_Freeze 之间切换,相关的参数输入框会根据引擎特性自动启用或隐藏。
-* **可视化版本属性**:无需编写复杂的版本配置文件,直接在界面文本框中输入版本号、公司名和产品描述,工具会自动将其写入生成的 `.exe` 元数据中。
-* **直观的资源添加**:通过交互式对话框选择需要打包的图片、数据文件夹,自动生成跨平台兼容的路径映射。
+* **双引擎无缝切换**:在下拉菜单中即可在 PyInstaller 和 Nuitka 之间灵活切换,相关的参数输入框与高级选项会根据所选引擎特性自动启用或隐藏。
+* **可视化版本属性**:无需编写复杂的版本配置文件,直接在界面文本框中输入版本号、公司名和产品描述,工具会自动将其写入生成的 `.exe` 元数据中,提升系统识别度。
+* **直观的资源添加**:通过交互式对话框选择需要打包的图片、数据文件夹,引擎会自动进行跨平台兼容的路径映射与资源封装。
---
### Changelog
-2.2
-* 更名为 QPyPack,规范目录结构for PyPI,优化内部沙盒与临时文件命名规范。
+
+**2.3.0**
+* **UI框架重构**:由 PyQt5 升级至 PySide6。优化了现代高分屏(High-DPI)下的文字与图标缩放表现。
+* **精简架构**:移除 `cx_Freeze` 打包引擎,专注打磨更现代、更高效的 PyInstaller 与 Nuitka 双核心引擎。
+* **Nuitka 官方最佳实践深度集成**:
+ * 单文件 (`--onefile`) 模式下,强制启用 `zstandard` 核心压缩环境,大幅优化产物体积与解压启动性能。
+ * 全局引入 `anti-bloat` 防膨胀插件,智能拦截并避免 `pytest`、`IPython` 等无用巨大依赖被误打包。
+ * 新增对 `tkinter` 官方探针的自动识别与插件激活。
+ * 重构附加资源打包逻辑,引擎将自动探测目标类型,严格区分并适配单文件 (`--include-data-files`) 与文件夹 (`--include-data-dir`) 打包规范。
+* **PyInstaller 深度优化**:
+ * 移除过时的强制 PyQt 隐藏导入逻辑,交由现代 Hook 自动完美处理。
+ * 彻底修复附加资源在跨平台构建时的路径分隔符 (`os.pathsep`) 匹配与切割异常问题。
+
+**2.2.0**
+* 更名为 QPyPack,规范目录结构 for PyPI,优化内部沙盒与临时文件命名规范。
* 超时守护 (Watchdog):引入看门狗机制,为关键打包步骤设置超时限制,防止因网络问题导致构建无限期挂起。
* 依赖推导优化:pipreqs 原生支持国内镜像站重定向加速,并增加 15 秒超时自愈,失败时自动降级为本地纯静态 AST 解析。
-* 智能命名输出:自动解析脚本版本,预设输出文件名为 {Name}_{Version} 格式(例如:QPyPack_2.2.0.exe)。
+* 智能命名输出:自动解析脚本版本,预设输出文件名为 `{Name}_{Version}` 格式(例如:QPyPack_2.2.0.exe)。
* 编译兼容扩展:Nuitka 编译模式新增对 PyQt6、PySide、NumPy、Matplotlib 等常用插件的自适应识别。
* 细节打磨:改进日志高频刷新滚动体验,优化版本信息元数据抓取退级逻辑。
-2.1
-* Nuitka单文件打包解压路径修改为%LOCALAPPDATA%
-* 增加建构失败动画
-* 尝试修复打包后容易被WINDOWS安全拦截的问题
+**2.1.0**
+* Nuitka 单文件打包解压路径修改为 `%LOCALAPPDATA%`。
+* 增加构建失败的 UI 动画反馈。
+* 尝试修复打包后容易被 Windows 安全中心误报拦截的问题。
+
+**2.0.1**
+* 修正 Nuitka 参数传递方式:将 `cmd.extend(["--include-data-files", "src=dest"])` 修改为更符合 Nuitka 规范的单参数追加形式:`cmd.append(f"--include-data-files={src}={dest}")`。
+* 加入平台感知后缀:动态识别操作系统(Windows 使用 `.exe` 后缀,而 macOS/Linux 不使用),避免跨平台定位产物失败。
+* 安全过滤平台专有参数:对于 Windows 特有的元数据写入(如公司名称、描述、文件版本、ICO 图标),只有当 `os.name == "nt"` 时才追加对应指令。
-2.0.1
-* 修正 Nuitka 参数传递方式:将 cmd.extend(["--include-data-files", "src=dest"]) 修改为更符合 Nuitka 规范的单参数追加形式:cmd.append(f"--include-data-files={src}={dest}")。
-* 加入平台感知后缀:动态识别操作系统(Windows 使用 .exe 后缀,而 macOS/Linux 不使用),避免跨平台定位产物失败。
-* 安全过滤平台专有参数:对于 Windows 特有的元数据写入(如公司名称、描述、文件版本、ICO 图标),只有当 os.name == "nt" 时才追加对应指令。
---
## 快速上手
@@ -61,12 +74,13 @@ QPyPack 是一个为 Python 开发者设计的图形化打包配置工具。它
---
### 启动应用
-Python >=3.8 环境中执行以下命令启动程序。
+在 Python >=3.8 环境中执行以下命令启动程序:
+
```bash
pip install qpypack
-```
-```bash
qpypack
```
-### 或者使用打包好的二进制文件直接运行
+
+### 预编译版本下载
+若不想配置 Python 环境,可直接下载打包好的独立可执行文件:
[GitHub Releases 下载链接](https://github.com/Qwejay/PyPack/releases)
diff --git a/pyproject.toml b/pyproject.toml
index 8b948d7..fa661f8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,17 +4,17 @@ build-backend = "setuptools.build_meta"
[project]
name = "qpypack"
-version = "2.2.0"
-description = "A modern graphical packaging config tool for PyInstaller, Nuitka, and cx_Freeze."
+version = "2.3.0"
+description = "A modern graphical packaging config tool for PyInstaller, Nuitka."
readme = "README.md"
authors = [
{ name = "QwejayHuang" }
]
-license = { text = "MIT" }
+license = { text = "GPL-3.0" }
requires-python = ">=3.8"
classifiers = [
"Programming Language :: Python :: 3",
- "License :: OSI Approved :: MIT License",
+ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: Microsoft :: Windows",
]
@@ -29,4 +29,4 @@ Homepage = "https://github.com/qwejay/QPyPack"
qpypack = "qpypack.main:main"
[tool.setuptools.package-data]
-qpypack = ["icon.ico"]
\ No newline at end of file
+qpypack = ["icon.ico"]
diff --git a/src/qpypack/__init__.py b/src/qpypack/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/src/qpypack/main.py b/src/qpypack/main.py
index da3e728..270e62b 100644
--- a/src/qpypack/main.py
+++ b/src/qpypack/main.py
@@ -2,9 +2,9 @@
# -*- coding: utf-8 -*-
__app_name__ = "QPyPack"
-__version__ = "2.2.0"
+__version__ = "2.3.0"
__author__ = "QwejayHuang"
-__company__ = "Qwesoft"
+__company__ = "QwejayHuang"
__description__ = "自动化 Python 脚本打包构建工具"
import sys
@@ -21,22 +21,25 @@ import math
import threading
from pathlib import Path
+if os.name == 'nt':
+ import winreg
+
os.environ["QT_LOGGING_RULES"] = "qt.text.font.db=false"
os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1"
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
-from PyQt5.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QWidget,
+from PySide6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QWidget,
QPushButton, QLabel, QLineEdit, QFileDialog, QCheckBox,
QComboBox, QFrame, QStackedLayout, QFormLayout, QTextEdit,
QGraphicsOpacityEffect, QGroupBox, QGridLayout, QTabWidget,
QMessageBox, QInputDialog, QFileIconProvider, QSizePolicy, QScrollArea,
QGraphicsDropShadowEffect)
-from PyQt5.QtCore import Qt, QThread, pyqtSignal, QPropertyAnimation, QEasingCurve, QParallelAnimationGroup, QFileInfo, QVariantAnimation, QTimer, QPointF, QRectF, QRect
-from PyQt5.QtGui import QFont, QDragEnterEvent, QDropEvent, QTextCursor, QIcon, QPixmap, QPainter, QColor, QPen
-from PyQt5.QtSvg import QSvgRenderer
+from PySide6.QtCore import Qt, QThread, Signal, QPropertyAnimation, QEasingCurve, QParallelAnimationGroup, QFileInfo, QVariantAnimation, QTimer, QPointF, QRectF, QRect
+from PySide6.QtGui import QFont, QDragEnterEvent, QDropEvent, QTextCursor, QIcon, QPixmap, QPainter, QColor, QPen
+from PySide6.QtSvg import QSvgRenderer
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.54 c0.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.96 c0.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',
+ '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',
@@ -54,11 +57,12 @@ MATERIAL_ICONS = {
def get_svg_icon(name, color="#5F6368", size=24):
path_data = MATERIAL_ICONS.get(name, "")
svg_str = f''
- renderer = QSvgRenderer(svg_str.encode('utf-8'))
+ renderer = QSvgRenderer()
+ renderer.load(svg_str.encode('utf-8'))
pixmap = QPixmap(size, size)
- pixmap.fill(Qt.transparent)
+ pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap)
- painter.setRenderHint(QPainter.Antialiasing)
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
renderer.render(painter)
painter.end()
return QIcon(pixmap)
@@ -69,7 +73,7 @@ def get_svg_pixmap(name, color="#5F6368", size=64):
def get_stdlib_names():
libs = {'os', 'sys', 're', 'math', 'time', 'datetime', 'json', 'urllib', 'sqlite3', 'csv',
- 'subprocess', 'shutil', 'threading', 'multprocessing', 'queue', 'socket',
+ 'subprocess', 'shutil', 'threading', 'multiprocessing', 'queue', 'socket',
'collections', 'itertools', 'functools', 'random', 'hashlib', 'base64',
'binascii', 'xml', 'logging', 'argparse', 'typing', 'pathlib', 'traceback',
'warnings', 'tempfile', 'platform', 'zipfile', 'tarfile', 'gzip', 'bz2',
@@ -80,18 +84,116 @@ def get_stdlib_names():
STD_LIBS = get_stdlib_names()
KNOWN_MAPPINGS = {
'win32com': 'pywin32', 'win32api': 'pywin32', 'win32con': 'pywin32', 'win32gui': 'pywin32',
- 'cv2': 'opencv-python', 'PIL': 'pillow', 'bs4': 'beautifulsoup4', 'sklearn': 'scikit-learn',
+ 'cv2': 'opencv-python', 'PIL': 'pillow', 'Pillow': 'pillow', 'bs4': 'beautifulsoup4', 'sklearn': 'scikit-learn',
'yaml': 'pyyaml', 'fitz': 'pymupdf', 'dotenv': 'python-dotenv'
}
def get_resource_path(relative_path):
if hasattr(sys, '_MEIPASS'):
- return os.path.join(sys._MEIPASS, relative_path)
- return os.path.join(os.path.dirname(os.path.abspath(__file__)), relative_path)
+ base_path = sys._MEIPASS
+ 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 find_system_python():
+ """全方位、深层次、抗毒化的核心级 Python 环境嗅探探针"""
+ candidates = []
+
+ for name in ('python', 'python3', 'pythonw'):
+ p = shutil.which(name)
+ if p: candidates.append(p)
+
+ if os.name == 'nt':
+ try:
+ py = shutil.which("py")
+ if py:
+ clean_env = os.environ.copy()
+ clean_env.pop("PYTHONHOME", None)
+ clean_env.pop("PYTHONPATH", None)
+ proc = subprocess.Popen(
+ [py, "-c", "import sys; print(sys.executable)"],
+ stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL,
+ text=True, env=clean_env, creationflags=subprocess.CREATE_NO_WINDOW
+ )
+ out, _ = proc.communicate(timeout=3)
+ if out and os.path.exists(out.strip()):
+ candidates.append(out.strip())
+ except: pass
+
+ try:
+ for hive in (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE):
+ try:
+ with winreg.OpenKey(hive, r"SOFTWARE\Python\PythonCore") as core_key:
+ for i in range(winreg.QueryInfoKey(core_key)[0]):
+ sub = winreg.EnumKey(core_key, i)
+ try:
+ 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
+
+ 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"
+ ]
+ for base in search_paths:
+ if base and os.path.exists(base):
+ try:
+ 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
+
+ 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)
+
+ for cand in candidates:
+ cand = os.path.normpath(cand)
+ if not os.path.exists(cand): continue
+
+ if os.name == 'nt' and "WindowsApps" in cand:
+ try:
+ if os.path.getsize(cand) == 0: continue
+ except: continue
+
+ clean_env = os.environ.copy()
+ clean_env.pop("PYTHONHOME", None)
+ clean_env.pop("PYTHONPATH", None)
+
+ try:
+ flags = subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
+ proc = subprocess.run(
+ [cand, "-V"], capture_output=True, env=clean_env,
+ creationflags=flags, timeout=3
+ )
+ if proc.returncode == 0:
+ return cand
+ except:
+ continue
+
+ return "python"
def get_python_executable():
- if getattr(sys, 'frozen', False): return shutil.which("python") or shutil.which("python3") or "python"
- return sys.executable
+ 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'):
+ return sys.executable
+
+ return find_system_python()
def remove_readonly(func, path, exc_info):
try: os.chmod(path, stat.S_IWRITE); func(path)
@@ -117,11 +219,11 @@ class AnimatedButton(QPushButton):
self.animation_group = QParallelAnimationGroup()
self.pos_anim = QPropertyAnimation(self, b"geometry")
self.pos_anim.setDuration(150)
- self.pos_anim.setEasingCurve(QEasingCurve.OutCubic)
+ self.pos_anim.setEasingCurve(QEasingCurve.Type.OutCubic)
self.op_anim = QPropertyAnimation(self.opacity_effect, b"opacity")
self.op_anim.setDuration(200)
- self.op_anim.setEasingCurve(QEasingCurve.InOutCubic)
+ self.op_anim.setEasingCurve(QEasingCurve.Type.InOutCubic)
self.is_hovered = False
@@ -161,7 +263,7 @@ class TargetIconWidget(QWidget):
self.pulse_value = 0
self.anim_timer = QTimer(self)
- self.anim_timer.setInterval(16) # ~60fps
+ self.anim_timer.setInterval(16)
self.anim_timer.timeout.connect(self._update_frame)
self.success_effect = QGraphicsDropShadowEffect(self)
@@ -176,7 +278,7 @@ class TargetIconWidget(QWidget):
self.burst_anim.setLoopCount(1)
self.burst_anim.setStartValue(0.0)
self.burst_anim.setEndValue(1.0)
- self.burst_anim.setEasingCurve(QEasingCurve.OutQuad)
+ self.burst_anim.setEasingCurve(QEasingCurve.Type.OutQuad)
self.burst_anim.valueChanged.connect(self._animate_burst)
self.shake_offset = 0
@@ -272,8 +374,8 @@ class TargetIconWidget(QWidget):
return
painter = QPainter(self)
- painter.setRenderHint(QPainter.Antialiasing)
- painter.setRenderHint(QPainter.SmoothPixmapTransform)
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
+ painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
center = self.rect().center()
center_x = center.x() + int(self.shake_offset)
@@ -284,7 +386,7 @@ class TargetIconWidget(QWidget):
radius = (self.current_size / 2) + 12
pen = QPen(QColor(26, 115, 232, 200))
pen.setWidth(4)
- pen.setCapStyle(Qt.RoundCap)
+ pen.setCapStyle(Qt.PenCapStyle.RoundCap)
painter.setPen(pen)
rect = QRectF(center_x - radius, center.y() - radius, radius * 2, radius * 2)
@@ -298,7 +400,7 @@ class TargetIconWidget(QWidget):
if self.burst_value < 1.0:
alpha = int(255 * (1.0 - self.burst_value))
- painter.setPen(Qt.NoPen)
+ painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(QColor(26, 115, 232, alpha))
burst_radius_1 = (self.current_size / 2) + 10 + self.burst_value * 40
@@ -324,20 +426,20 @@ class TargetIconWidget(QWidget):
draw_size,
draw_size
)
- scaled_pix = self.pixmap.scaled(draw_size, draw_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
+ scaled_pix = self.pixmap.scaled(draw_size, draw_size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
painter.drawPixmap(pix_rect, scaled_pix)
painter.end()
class DropArea(QFrame):
- fileDropped = pyqtSignal(str)
+ fileDropped = Signal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("DropArea")
self.setAcceptDrops(True)
- self.setFrameStyle(QFrame.NoFrame)
- self.setCursor(Qt.PointingHandCursor)
+ self.setFrameStyle(QFrame.Shape.NoFrame)
+ self.setCursor(Qt.CursorShape.PointingHandCursor)
self.setStyleSheet("""
#DropArea { background-color: #f8f9fa; border: 2px dashed #dadce0; border-radius: 12px; }
#DropArea:hover { background-color: #f1f3f4; border: 2px dashed #bdc1c6; }
@@ -371,14 +473,14 @@ class DropArea(QFrame):
layout.addSpacing(18)
self.label = QLabel("将脚本(.py)拖拽至此处\n或 点击浏览")
- self.label.setAlignment(Qt.AlignCenter)
+ self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
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.AlignCenter)
+ self.sub_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.sub_label.setStyleSheet("QLabel { background: transparent; color: #9AA0A6; font-size: 13px; border: none; }")
layout.addWidget(self.sub_label)
@@ -399,7 +501,7 @@ class DropArea(QFrame):
self.fileDropped.emit(urls[0].toLocalFile())
def mousePressEvent(self, event):
- if event.button() == Qt.LeftButton:
+ if event.button() == Qt.MouseButton.LeftButton:
fp, _ = QFileDialog.getOpenFileName(self, "选择 Python 源代码文件", "", "Python Scripts (*.py)")
if fp: self.fileDropped.emit(fp)
@@ -524,7 +626,7 @@ class SettingsPanel(QWidget):
self.btn_save = AnimatedButton(" 保存并返回工作区")
self.btn_save.setFixedHeight(44)
- self.btn_save.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ self.btn_save.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
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.show_main)
@@ -543,7 +645,7 @@ class SettingsPanel(QWidget):
def build_basic_tab(self):
scroll = QScrollArea()
scroll.setWidgetResizable(True)
- scroll.setFrameShape(QScrollArea.NoFrame)
+ scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setStyleSheet("background: transparent;")
content = QWidget()
@@ -553,12 +655,12 @@ class SettingsPanel(QWidget):
grp_core = QGroupBox("核心参数")
form_core = QFormLayout(grp_core)
- form_core.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
+ form_core.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
form_core.setContentsMargins(15, 20, 15, 15)
form_core.setVerticalSpacing(12)
self.engine_combo = QComboBox()
- self.engine_combo.addItems(["PyInstaller", "Nuitka", "cx_Freeze"])
+ self.engine_combo.addItems(["PyInstaller", "Nuitka"])
self.engine_combo.currentIndexChanged.connect(self.on_engine_changed)
form_core.addRow("构建引擎:", self.engine_combo)
@@ -610,7 +712,7 @@ class SettingsPanel(QWidget):
def build_env_tab(self):
scroll = QScrollArea()
scroll.setWidgetResizable(True)
- scroll.setFrameShape(QScrollArea.NoFrame)
+ scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setStyleSheet("background: transparent;")
content = QWidget()
@@ -620,14 +722,14 @@ class SettingsPanel(QWidget):
grp_env = QGroupBox("隔离与网络")
form_env = QFormLayout(grp_env)
- form_env.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
+ form_env.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
form_env.setContentsMargins(15, 20, 15, 15)
form_env.setVerticalSpacing(12)
self.pip_source_edit = QLineEdit("https://pypi.tuna.tsinghua.edu.cn/simple")
form_env.addRow("PIP 镜像:", self.pip_source_edit)
- self.venv_check = QCheckBox("启用独立虚拟环境 (推荐,保证依赖纯净)")
+ self.venv_check = QCheckBox("启用独立打包环境")
self.venv_check.setChecked(True)
form_env.addRow("", self.venv_check)
lay.addWidget(grp_env)
@@ -647,7 +749,7 @@ class SettingsPanel(QWidget):
dep_lay.addLayout(grid_dep)
form_dep = QFormLayout()
- form_dep.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
+ form_dep.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
self.hidden_edit = QLineEdit()
self.hidden_edit.setPlaceholderText("例如: pandas, PyQt5 (逗号分隔)")
btn_scan = QPushButton("AST 扫描")
@@ -670,7 +772,7 @@ class SettingsPanel(QWidget):
def build_adv_tab(self):
scroll = QScrollArea()
scroll.setWidgetResizable(True)
- scroll.setFrameShape(QScrollArea.NoFrame)
+ scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setStyleSheet("background: transparent;")
content = QWidget()
@@ -680,12 +782,12 @@ class SettingsPanel(QWidget):
grp_res = QGroupBox("附加资源与优化")
form_res = QFormLayout(grp_res)
- form_res.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
+ form_res.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
form_res.setContentsMargins(15, 20, 15, 15)
form_res.setVerticalSpacing(12)
self.add_data_edit = QLineEdit()
- self.add_data_edit.setPlaceholderText("例如: data/*:data (跨平台自动处理路径)")
+ self.add_data_edit.setPlaceholderText("例如: data/model.bin:data")
btn_add = QPushButton("添加...")
btn_add.setProperty("class", "ToolBtn")
btn_add.clicked.connect(self.add_resource)
@@ -694,7 +796,7 @@ class SettingsPanel(QWidget):
h_res.addWidget(btn_add)
form_res.addRow("附加资源:", h_res)
- self.upx_check = QCheckBox("启用 UPX 压缩优化 (仅 PyInstaller)")
+ self.upx_check = QCheckBox("启用 PyInstaller UPX")
form_res.addRow("", self.upx_check)
lay.addWidget(grp_res)
lay.addStretch()
@@ -707,7 +809,7 @@ class SettingsPanel(QWidget):
def build_meta_tab(self):
scroll = QScrollArea()
scroll.setWidgetResizable(True)
- scroll.setFrameShape(QScrollArea.NoFrame)
+ scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setStyleSheet("background: transparent;")
content = QWidget()
@@ -717,7 +819,7 @@ class SettingsPanel(QWidget):
self.grp_meta = QGroupBox("版本元数据 (Metadata)")
meta_form = QFormLayout(self.grp_meta)
- meta_form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
+ meta_form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
meta_form.setContentsMargins(15, 20, 15, 15)
meta_form.setVerticalSpacing(12)
@@ -729,7 +831,7 @@ class SettingsPanel(QWidget):
meta_form.addRow("文件描述:", self.ver_desc)
lay.addWidget(self.grp_meta)
- tip = QLabel("注:仅 PyInstaller 和 Nuitka 引擎支持写入 EXE 版本信息。")
+ tip = QLabel("注:写入 EXE 版本信息可提升系统识别度。")
tip.setStyleSheet("color: #9AA0A6; font-size: 12px; font-weight: normal; background: transparent;")
lay.addWidget(tip)
lay.addStretch()
@@ -741,15 +843,13 @@ class SettingsPanel(QWidget):
def on_engine_changed(self):
engine = self.engine_combo.currentText()
- self.onefile_check.setVisible(engine in ("PyInstaller", "Nuitka"))
self.upx_check.setVisible(engine == "PyInstaller")
- self.tabs.setTabEnabled(3, engine != "cx_Freeze")
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.KeepAspectRatio, Qt.SmoothTransformation))
+ self.icon_preview.setPixmap(pixmap.scaled(28, 28, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation))
return
self.icon_preview.clear()
@@ -783,8 +883,8 @@ class SettingsPanel(QWidget):
except Exception as e: QMessageBox.warning(self, "错误", f"扫描失败: {e}")
def add_resource(self):
- choice = QMessageBox.question(self, "添加资源", "Yes=文件, No=文件夹", QMessageBox.Yes | QMessageBox.No)
- if choice == QMessageBox.Yes:
+ choice = QMessageBox.question(self, "添加资源", "Yes=文件, No=文件夹", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
+ if choice == QMessageBox.StandardButton.Yes:
files, _ = QFileDialog.getOpenFileNames(self, "选择文件", "", "All Files (*)")
for f in files:
f = Path(f).resolve().as_posix()
@@ -808,8 +908,8 @@ class SettingsPanel(QWidget):
# ======================== 核心构建引擎 ========================
class PackingThread(QThread):
- progress = pyqtSignal(str)
- finished = pyqtSignal(bool, str)
+ progress = Signal(str)
+ finished = Signal(bool, str)
def __init__(self, params):
super().__init__()
@@ -834,8 +934,13 @@ class PackingThread(QThread):
timer = None
is_timeout = [False]
+ clean_env = os.environ.copy()
+ clean_env.pop("PYTHONHOME", None)
+ clean_env.pop("PYTHONPATH", None)
+
try:
- kwargs = {"stdout": subprocess.PIPE, "stderr": subprocess.STDOUT, "cwd": cwd, "text": True, "errors": "replace"}
+ kwargs = {"stdout": subprocess.PIPE, "stderr": subprocess.STDOUT, "cwd": cwd,
+ "text": True, "errors": "replace", "env": clean_env}
if os.name == 'nt': kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
self.process = subprocess.Popen(cmd, **kwargs)
@@ -867,6 +972,13 @@ class PackingThread(QThread):
return False
return self.process.returncode == 0
+ except FileNotFoundError as e:
+ cmd_name = cmd[0] if isinstance(cmd, list) and cmd else str(cmd)
+ if "python" in str(cmd_name).lower():
+ self.progress.emit(f"[环境缺失] 无法找到系统宿主 Python。请确保您已正确安装 Python 并添加至系统 PATH。")
+ else:
+ self.progress.emit(f"[系统异常] 找不到可执行程序: {cmd_name}。详细报错: {e}")
+ return False
except Exception as e:
self.progress.emit(f"[系统异常] 进程执行失败: {e}")
return False
@@ -892,7 +1004,7 @@ class PackingThread(QThread):
ext = ".exe" if os.name == "nt" else ""
try:
- self.progress.emit("[清理] 初始化构建管线...")
+ self.progress.emit("[清理] 初始化构建空间...")
robust_rmtree(Path.cwd() / "build")
robust_rmtree(Path.cwd() / "dist")
@@ -903,25 +1015,49 @@ class PackingThread(QThread):
if not build_script_path and err_msg: return self.finished.emit(False, f"[安全拦截] {err_msg}")
script_posix = build_script_path.as_posix()
+ script_imports = set()
+ required_pkgs_normalized = set()
+ try:
+ code_text = build_script_path.read_text(encoding='utf-8', errors='ignore')
+ try:
+ for node in ast.walk(ast.parse(code_text)):
+ if isinstance(node, ast.Import):
+ for alias in node.names: script_imports.add(alias.name.split('.')[0])
+ elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module:
+ script_imports.add(node.module.split('.')[0])
+ except:
+ script_imports = set(re.findall(r'^\s*(?:from|import)\s+([a-zA-Z0-9_]+)', code_text, re.M))
+
+ required_pkgs_normalized = {KNOWN_MAPPINGS.get(m, m).lower().replace('_', '-') for m in script_imports if m not in STD_LIBS}
+ except Exception as e:
+ self.progress.emit(f"[依赖预检] 解析依赖失败: {e}")
+
+ system_python_exe = get_python_executable()
+ self.progress.emit(f"[环境] Python: {system_python_exe}")
+
if self.params['use_venv']:
- self.progress.emit("[环境] 初始化虚拟隔离沙盒...")
+ self.progress.emit("[环境] 初始化独立打包环境...")
self.venv_dir = Path(tempfile.mkdtemp(prefix="qpypack_env_")).resolve()
- if not self.run_cmd([get_python_executable(), "-m", "venv", self.venv_dir.as_posix()]):
- return self.finished.emit(False, "虚拟环境初始化失败。")
+ if not self.run_cmd([system_python_exe, "-m", "venv", self.venv_dir.as_posix()]):
+ return self.finished.emit(False, "虚拟环境初始化失败。这通常是因为您的 Python 环境被精简或已损坏。")
python_exe = (self.venv_dir / ("Scripts/python.exe" if os.name == "nt" else "bin/python")).as_posix()
- else: python_exe = Path(get_python_executable()).resolve().as_posix()
+ else:
+ python_exe = system_python_exe
pip_args = ["-i", pip_idx] if pip_idx else []
- engine_pkg = "nuitka[onefile]" if engine == "Nuitka" else "cx_Freeze" if engine == "cx_Freeze" else "pyinstaller"
+ engine_pkg = "nuitka" if engine == "Nuitka" else "pyinstaller"
self.progress.emit(f"[环境] 下载核心组件...")
core_pkgs = [engine_pkg]
- if engine == "PyInstaller": core_pkgs.append("pillow")
+ if engine == "PyInstaller":
+ core_pkgs.append("pillow")
+ elif engine == "Nuitka":
+ core_pkgs.append("zstandard")
+
self.run_cmd([python_exe, "-m", "pip", "install", "-q"] + core_pkgs + pip_args)
req_success = False
- # 1. 优先尝试部署 requirements.txt
if self.params.get('use_reqs'):
req_file = script_dir / "requirements.txt"
if req_file.exists():
@@ -938,9 +1074,9 @@ class PackingThread(QThread):
temp_req.unlink(missing_ok=True)
except Exception as e: self.progress.emit(f"[警告] {e}")
- # 2. requirements.txt 未能成功解决依赖,则启用 pipreqs(带镜像站原生参数及 15 秒超时自愈)
if self.params.get('use_pipreqs') and not req_success:
self.progress.emit("[依赖] 执行 pipreqs 深度推导...")
+
self.run_cmd([python_exe, "-m", "pip", "install", "pipreqs", "-q"] + pip_args)
temp_pipreqs = script_dir / "pypack_pipreqs_out.txt"
@@ -962,33 +1098,33 @@ class PackingThread(QThread):
if self.run_cmd(pipreqs_cmd, timeout=15):
if temp_pipreqs.exists():
+ pipreqs_incomplete = False
+ try:
+ pipreqs_text = temp_pipreqs.read_text(encoding='utf-8', errors='ignore').lower().replace('_', '-')
+ for pkg in required_pkgs_normalized:
+ if not re.search(r'\b' + re.escape(pkg) + r'\b', pipreqs_text):
+ pipreqs_incomplete = True
+ self.progress.emit(f"[警告] pipreqs 未能成功匹配装载该模块: {pkg}")
+ except:
+ pipreqs_incomplete = True
+
if self.run_cmd([python_exe, "-m", "pip", "install", "-q", "-r", temp_pipreqs.as_posix()] + pip_args):
- req_success = True
+ if not pipreqs_incomplete:
+ req_success = True
+ else:
+ self.progress.emit("[依赖] 检测到部分核心模块漏报,准备进入本地解析...")
temp_pipreqs.unlink(missing_ok=True)
else:
- self.progress.emit("[提示] pipreqs 深度解析超时或失败,转为本地 AST 解析...")
- if temp_pipreqs.exists():
- try: temp_pipreqs.unlink()
- except: pass
+ self.progress.emit("[提示] pipreqs 深度解析超时或失败...")
- # 3. 如果以上方案均未成功解决依赖,则采用本地纯静态 AST 降级解析兜底
if not req_success and (self.params.get('use_reqs') or self.params.get('use_pipreqs')):
- self.progress.emit("[依赖] 正在启用 AST 本地语法树解析机制兜底...")
+ self.progress.emit("[依赖] 正在启用 AST 本地语法树解析...")
try:
- code = build_script_path.read_text(encoding='utf-8', errors='ignore')
- third_party = set()
- try:
- for node in ast.walk(ast.parse(code)):
- if isinstance(node, ast.Import):
- for alias in node.names: third_party.add(alias.name.split('.')[0])
- elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module:
- third_party.add(node.module.split('.')[0])
- except: third_party = set(re.findall(r'^\s*(?:from|import)\s+([a-zA-Z0-9_]+)', code, re.M))
-
- pkgs_to_install = [KNOWN_MAPPINGS.get(m, m) for m in third_party if m not in STD_LIBS]
+ pkgs_to_install = [KNOWN_MAPPINGS.get(m, m) for m in script_imports if m not in STD_LIBS]
if pkgs_to_install:
self.progress.emit(f"[依赖] 同步外部模块: {', '.join(pkgs_to_install)}")
- self.run_cmd([python_exe, "-m", "pip", "install", "-q"] + pkgs_to_install + pip_args)
+ if not self.run_cmd([python_exe, "-m", "pip", "install", "-q"] + pkgs_to_install + pip_args):
+ self.progress.emit("[警告] 部分外部依赖在沙盒中安装失败,这可能导致最终打包的 EXE 运行报错!")
except Exception as e: self.progress.emit(f"[异常] {e}")
if self._is_cancelled: return self.finished.emit(False, "构建已被中断。")
@@ -1000,20 +1136,16 @@ class PackingThread(QThread):
if engine == "PyInstaller":
self.temp_workpath = Path(tempfile.mkdtemp(prefix="qpypack_build_")).resolve()
- cmd = [python_exe, "-m", "PyInstaller", "--clean", "-y", f"--workpath={self.temp_workpath.as_posix()}", f"--name={app_name}"]
+ cmd = [python_exe, "-m", "PyInstaller", "--clean", "--noconfirm", f"--workpath={self.temp_workpath.as_posix()}", f"--name={app_name}"]
- if self.params.get('onefile'):
- cmd.append("--onefile")
- else:
- cmd.append("--onedir")
+ if self.params['onefile']: cmd.append("--onefile")
+ else: cmd.append("--onedir")
- if self.params.get('noconsole'):
- cmd.append("--noconsole")
+ if self.params['noconsole']: cmd.append("--noconsole")
if icon_path:
cmd.extend(["--icon", icon_path])
- sep = ';' if os.name == 'nt' else ':'
- cmd.extend(["--add-data", f"{icon_path}{sep}."])
+ cmd.extend(["--add-data", f"{icon_path}{os.pathsep}."])
if self.params.get('version_file') and os.name == "nt": cmd.extend(["--version-file", self.params['version_file']])
if self.params.get('upx'):
@@ -1025,11 +1157,9 @@ class PackingThread(QThread):
for d in self.params.get('add_data', '').split(','):
d = d.strip()
if d:
- d = d.replace('\\', '/')
- if os.name == 'nt' and ':' in d and ';' not in d:
- parts = d.rsplit(':', 1)
- if len(parts) == 2: d = f"{parts[0]};{parts[1]}"
- cmd.extend(["--add-data", d])
+ parts = d.rsplit(':', 1)
+ if len(parts) == 2:
+ cmd.extend(["--add-data", f"{parts[0].strip()}{os.pathsep}{parts[1].strip()}"])
elif engine == "Nuitka":
self.temp_out_dir = Path(tempfile.mkdtemp(prefix="nuitka_out_")).resolve()
@@ -1038,68 +1168,50 @@ class PackingThread(QThread):
cmd.append(f"--jobs={os.cpu_count() or 4}")
- if self.params.get('onefile'): cmd.append("--onefile")
+ if self.params['onefile']: cmd.append("--onefile")
else: cmd.append("--standalone")
- if self.params.get('noconsole'): cmd.append("--windows-console-mode=disable")
+
+ if self.params['noconsole']: cmd.append("--windows-console-mode=disable")
if icon_path:
if os.name == "nt":
cmd.append(f"--windows-icon-from-ico={icon_path}")
elif sys.platform == "darwin" and icon_path.endswith(".icns"):
cmd.append(f"--macos-app-icon={icon_path}")
- cmd.append(f"--include-data-files={icon_path}={Path(icon_path).name}")
+ 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"--product-name={self.params['ver_desc']}")
if self.params.get('ver_ver'): cmd.append(f"--file-version={self.params['ver_ver']}")
- try:
- script_text = build_script_path.read_text(encoding='utf-8', errors='ignore')
- if 'PyQt5' in script_text:
- cmd.append("--enable-plugin=pyqt5")
- elif 'PyQt6' in script_text:
- cmd.append("--enable-plugin=pyqt6")
- elif 'PySide2' in script_text:
- cmd.append("--enable-plugin=pyside2")
- elif 'PySide6' in script_text:
- cmd.append("--enable-plugin=pyside6")
- elif 'numpy' in script_text:
- cmd.append("--enable-plugin=numpy")
- elif 'matplotlib' in script_text:
- cmd.append("--enable-plugin=matplotlib")
- except: pass
+ cmd.append("--enable-plugin=anti-bloat")
+
+ if 'PyQt5' in script_imports: cmd.append("--enable-plugin=pyqt5")
+ elif 'PyQt6' in script_imports: cmd.append("--enable-plugin=pyqt6")
+ elif 'PySide2' in script_imports: cmd.append("--enable-plugin=pyside2")
+ elif 'PySide6' in script_imports: cmd.append("--enable-plugin=pyside6")
+
+ if 'numpy' in script_imports: cmd.append("--enable-plugin=numpy")
+ if 'matplotlib' in script_imports: cmd.append("--enable-plugin=matplotlib")
+ if 'tkinter' in script_imports: cmd.append("--enable-plugin=tk-inter")
for imp in self.params.get('hidden_imports', '').split(','):
if imp.strip(): cmd.append(f"--include-module={imp.strip()}")
- for d in self.params.get('add_data', '').split(','):
- d = d.strip()
- if d:
- d = d.replace('\\', '/')
- parts = d.split(':', 1)
- if len(parts) == 2:
- cmd.append(f"--include-data-files={parts[0]}={parts[1]}")
-
- elif engine == "cx_Freeze":
- cmd = [python_exe, "-m", "cx_Freeze", script_posix, f"--target-dir=dist/{app_name}", f"--target-name={app_name}{ext}"]
- if self.params.get('noconsole'): cmd.append("--base=Win32GUI")
- else: cmd.append("--base=Console")
- add_data_list = []
- if icon_path:
- cmd.extend(["--icon", icon_path])
- add_data_list.append(f"{icon_path}={Path(icon_path).name}")
- for imp in self.params.get('hidden_imports', '').split(','):
- if imp.strip(): cmd.extend(["--include-modules", imp.strip()])
for d in self.params.get('add_data', '').split(','):
d = d.strip()
if d:
- d = d.replace('\\', '/')
- parts = d.split(':', 1)
- if len(parts) == 2: add_data_list.append(f"{parts[0]}={parts[1]}")
- if add_data_list: cmd.append(f"--include-files={','.join(add_data_list)}")
+ parts = d.rsplit(':', 1)
+ if len(parts) == 2:
+ src_path = Path(parts[0].strip())
+ dst_path = parts[1].strip()
+ if src_path.is_dir():
+ cmd.append(f"--include-data-dir={src_path.resolve().as_posix()}={dst_path}")
+ else:
+ cmd.append(f"--include-data-files={src_path.resolve().as_posix()}={dst_path}")
- if engine in ["PyInstaller", "Nuitka"]: cmd.append(script_posix)
+ cmd.append(script_posix)
success = self.run_cmd(cmd, cwd=Path.cwd().resolve().as_posix())
if self._is_cancelled: return self.finished.emit(False, "构建已被中断。")
@@ -1108,7 +1220,6 @@ class PackingThread(QThread):
cwd = Path.cwd().resolve()
if engine == "PyInstaller": src_out = cwd / "dist" / (f"{app_name}{ext}" if self.params['onefile'] else app_name)
elif engine == "Nuitka": src_out = self.temp_out_dir / (f"{app_name}{ext}" if self.params['onefile'] else f"{app_name}.dist")
- elif engine == "cx_Freeze": src_out = cwd / "dist" / app_name
final_out = script_dir / src_out.name
if success and src_out.exists():
@@ -1132,7 +1243,7 @@ class PackingThread(QThread):
try: build_script_path.unlink()
except: pass
- if self.params.get('clean_all'):
+ if self.params['clean_all']:
self.progress.emit("[清理] 抹除环境与沙盒...")
for p in [self.venv_dir, self.temp_workpath, self.temp_out_dir]:
if p and p.exists(): robust_rmtree(p)
@@ -1231,7 +1342,7 @@ class MainWindow(QMainWindow):
self.btn_main = AnimatedButton("")
self.btn_main.setFixedHeight(44)
- self.btn_main.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ self.btn_main.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self.btn_main.clicked.connect(self.on_main_btn_clicked)
btn_layout.addWidget(self.btn_main)
@@ -1325,7 +1436,7 @@ class MainWindow(QMainWindow):
def _animate_switch(self, target_widget):
self.anim = QPropertyAnimation(self.stacked_layout.currentWidget(), b"geometry")
self.anim.setDuration(250)
- self.anim.setEasingCurve(QEasingCurve.OutCubic)
+ self.anim.setEasingCurve(QEasingCurve.Type.OutCubic)
self.anim.setStartValue(self.stacked_layout.currentWidget().geometry())
self.stacked_layout.setCurrentWidget(target_widget)
self.anim.setEndValue(target_widget.geometry())
@@ -1341,7 +1452,6 @@ class MainWindow(QMainWindow):
self.script_path = path
- # 智能元数据预设逻辑
app_name = Path(path).stem
version = ""
author = "My Studio"
@@ -1350,13 +1460,11 @@ class MainWindow(QMainWindow):
try:
content = Path(path).read_text(encoding='utf-8', errors='ignore')
- # 解析版本
v_match = re.search(r'^(?:__version__|VERSION|version)\s*=\s*[\'"]([^\'"]+)[\'"]', content, re.M | re.I)
if v_match:
version = v_match.group(1)
self.settings_panel.ver_ver.setText(version)
- # 解析发行公司(无company则降级尝试绑定author)
c_match = re.search(r'^(?:__company__|COMPANY)\s*=\s*[\'"]([^\'"]+)[\'"]', content, re.M | re.I)
if c_match:
author = c_match.group(1)
@@ -1365,12 +1473,10 @@ class MainWindow(QMainWindow):
if a_match: author = a_match.group(1)
self.settings_panel.ver_comp.setText(author)
- # 解析名称
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)
@@ -1378,11 +1484,9 @@ class MainWindow(QMainWindow):
except: pass
- # 拼装生成带版本后缀的默认文件名
default_output_name = f"{app_name}_{version}" if version else app_name
self.settings_panel.name_edit.setText(default_output_name)
- # 图标自动配对
script_dir = Path(path).parent
auto_icon = None
@@ -1436,7 +1540,7 @@ class MainWindow(QMainWindow):
'engine': engine,
'script_path': self.script_path,
'app_name': app_name,
- 'onefile': sp.onefile_check.isChecked() if engine != "cx_Freeze" else False,
+ 'onefile': sp.onefile_check.isChecked(),
'noconsole': sp.noconsole_check.isChecked(),
'icon': sp.icon_edit.text().strip(),
'use_reqs': sp.reqs_check.isChecked(),
@@ -1447,9 +1551,9 @@ class MainWindow(QMainWindow):
'use_venv': sp.venv_check.isChecked(),
'clean_all': sp.clean_all_check.isChecked(),
'version_file': version_file.as_posix() if version_file else None,
- 'ver_comp': sp.ver_comp.text() if engine != "cx_Freeze" else "",
- 'ver_desc': sp.ver_desc.text() if engine != "cx_Freeze" else "",
- 'ver_ver': sp.ver_ver.text() if engine != "cx_Freeze" else "",
+ 'ver_comp': sp.ver_comp.text(),
+ 'ver_desc': sp.ver_desc.text(),
+ 'ver_ver': sp.ver_ver.text(),
'pip_index_url': sp.pip_source_edit.text().strip()
}
@@ -1507,17 +1611,12 @@ class MainWindow(QMainWindow):
def main():
- if hasattr(Qt, 'HighDpiScaleFactorRoundingPolicy'):
- QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
- QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
- QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
-
app = QApplication(sys.argv)
app.setStyle("Fusion")
app.setFont(QFont("Microsoft YaHei", 9))
win = MainWindow()
win.show()
- sys.exit(app.exec_())
+ sys.exit(app.exec())
if __name__ == "__main__":
- main()
\ No newline at end of file
+ main()