diff --git a/README.md b/README.md index 2f967f6..8312934 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ QPyPack 是一款致力于简化 Python 应用打包流程的可视化工具。 您可以通过以下两种方式之使用 QPyPack: -### 方法一:通过 pip 安装并运行(推荐开发者使用) +### 方法一:通过 pip 安装并运行 在 Python >= 3.8 的环境中,执行以下命令进行安装与启动: @@ -70,7 +70,7 @@ pip install qpypack qpypack ``` -### 方法二:下载免安装独立运行版(推荐普通用户使用) +### 方法二:下载二进制文件 如果您不想配置本地 Python 环境,可直接在 Release 页面下载对应系统的预编译打包版本: 👉 [下载页面](https://github.com/Qwejay/QPyPack/releases) @@ -79,6 +79,19 @@ qpypack ## 📅 更新日志 (Changelog) +### [2.5.5] - 2026-07-22 + +* **消除误删风险**:优化目录 `build/dist` 智能清理,保护桌面及工作区安全。 +* **全沙盒隔离编译**:引入系统级临时目录隔离输出,彻底解决跨盘符与路径割裂异常。 +* **修复路径映射与 UX**:构建 cwd 重定向至源码目录以完美解析相对路径;修正 `open_dist` 打开逻辑。 +* **修复 pipreqs 崩溃**:尝试解决依赖自动扫描过程中的编码中断问题。 + +### [2.5.4] - 2026-07-18 + +* **新增精简模式**:自动剔除 `pip`、`unittest` 等冗余库并启用 Nuitka `-OO` 优化,大幅缩减产物体积;配套深度源码静态扫描(至多检索 500 个本地依赖),保障极致精简下的运行稳定性。 +* **修复 UPX 假性报错问题**:当未勾选 UPX 时,显式向引擎传递 `--noupx` 指令,阻断 PyInstaller 静默调用系统 UPX 造成的兼容性报错与日志干扰。 +* **修复日志中文乱码问题**:对底层命令行调用 (`subprocess`) 全面强制追加 `encoding="utf-8"` 参数,彻底解决 Windows 默认编码 (GBK) 解析字节流时引发的控制台乱码。。 + ### [2.5.3] - 2026-07-17 * **引擎版本锁定**:支持在高级设置中自定义 Nuitka/PyInstaller/pipreqs 的版本,杜绝上游更新导致的打包失败。 diff --git a/pyproject.toml b/pyproject.toml index f47c3f1..5555c8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "qpypack" -version = "2.5.4" +version = "2.5.5" description = "A cross-platform GUI packaging and compilation suite for PyInstaller and Nuitka." readme = "README.md" authors = [ diff --git a/qpypack/main.py b/qpypack/main.py index 3d9fd66..e48fc07 100644 --- a/qpypack/main.py +++ b/qpypack/main.py @@ -36,7 +36,7 @@ from PySide6.QtGui import QFont, QDragEnterEvent, QDropEvent, QTextCursor, QIcon from PySide6.QtSvg import QSvgRenderer __app_name__ = "QPyPack" -__version__ = "2.5.4" +__version__ = "2.5.5" __author__ = "QwejayHuang" __company__ = "QwejayHuang" __description__ = "基于 PyInstaller 与 Nuitka 的跨平台 Python 应用打包构建工具" @@ -1421,11 +1421,11 @@ class SettingsPanel(QWidget): card2, lay2 = self._create_card("编译选项") lay2.setSpacing(16) - self.concise_log_check = QCheckBox("精简日志输出 (过滤构建引擎产生的警告信息)") - self.auto_save_log_check = QCheckBox("自动保存编译日志 (构建失败时在输出目录落盘生成日志文件)") - self.auto_icon_check = QCheckBox("自动检索图标 (自动匹配并绑定源文件同名 ICO/SVG 图标)") - self.clean_all_check = QCheckBox("清理构建缓存 (构建完成后自动删除临时目录与缓存文件)") - self.sound_notify_check = QCheckBox("启用构建提示音 (构建成功或失败时播放系统音效)") + self.concise_log_check = QCheckBox("精简日志输出") + self.auto_save_log_check = QCheckBox("自动保存编译日志") + self.auto_icon_check = QCheckBox("自动检索图标") + self.clean_all_check = QCheckBox("清理构建缓存") + self.sound_notify_check = QCheckBox("启用构建提示音") 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) @@ -1685,6 +1685,7 @@ class PackingThread(QThread): self.venv_dir = None self.temp_workpath = None self.temp_out_dir = None + self.temp_dist_dir = None self.all_raw_logs = [] def cancel(self): @@ -1695,7 +1696,7 @@ class PackingThread(QThread): else: self.process.kill() except: pass - def run_cmd(self, cmd, cwd=None, timeout=None): + def run_cmd(self, cmd, cwd=None, timeout=None, silent_error=False): if self._is_cancelled: return False timer = None @@ -1769,7 +1770,7 @@ class PackingThread(QThread): success = self.process.returncode == 0 - if not success and self.params.get('concise_log', True) and cmd_raw_lines: + if not success and not silent_error and self.params.get('concise_log', True) and cmd_raw_lines: self.progress.emit("\n" + "!"*10 + " [诊断回溯: 以下是由于该环节执行异常产生的完整原始日志] " + "!"*10) self.progress.emit('\n'.join(cmd_raw_lines)) self.progress.emit("!"*60 + "\n") @@ -1867,9 +1868,7 @@ class PackingThread(QThread): ext = ".exe" if os.name == "nt" else "" try: - self.progress.emit("[INFO] 正在初始化工作空间并清除历史构建缓存...") - robust_rmtree(Path.cwd() / "build") - robust_rmtree(Path.cwd() / "dist") + self.progress.emit("[INFO] 正在初始化隔离构建环境...") script_path = Path(self.params['script_path']).resolve() script_dir = script_path.parent @@ -1970,15 +1969,14 @@ class PackingThread(QThread): self.progress.emit(f"[INFO] 依赖分析服务源地址: {pypi_server}") self.progress.emit("[INFO] 正在查询各依赖库的版本,请稍候...") - success_pipreqs = self.run_cmd(pipreqs_cmd, timeout=15) + success_pipreqs = self.run_cmd(pipreqs_cmd, timeout=15, silent_error=True) if not success_pipreqs: - self.progress.emit("[INFO] UTF-8 扫描受阻,正在尝试利用系统本地编码重新扫描...") - fallback_cmd = [c for c in pipreqs_cmd if c != "utf-8"] - if "--encoding" in fallback_cmd: - idx = fallback_cmd.index("--encoding") - fallback_cmd.pop(idx) - success_pipreqs = self.run_cmd(fallback_cmd, timeout=15) + self.progress.emit("[INFO] UTF-8 扫描受阻,正在尝试利用兼容编码重新扫描...") + fallback_cmd = ["iso-8859-1" if c == "utf-8" else c for c in pipreqs_cmd] + success_pipreqs = self.run_cmd(fallback_cmd, timeout=15, silent_error=True) + if not success_pipreqs: + self.progress.emit("[WARN] pipreqs 深度扫描因编码异常中断,已跳过。将由后续 AST 扫描引擎补全依赖。") if success_pipreqs and temp_pipreqs.exists(): self.run_cmd([python_exe, "-m", "pip", "install", "-q", "-r", temp_pipreqs.as_posix()] + pip_args) @@ -2014,10 +2012,12 @@ class PackingThread(QThread): if engine == "PyInstaller": self.temp_workpath = Path(tempfile.mkdtemp(prefix="qpypack_build_")).resolve() + self.temp_dist_dir = Path(tempfile.mkdtemp(prefix="qpypack_dist_")).resolve() cmd = [ python_exe, "-m", "PyInstaller", "--clean", "--noconfirm", + f"--distpath={self.temp_dist_dir.as_posix()}", f"--workpath={self.temp_workpath.as_posix()}", f"--name={app_name}" ] @@ -2160,18 +2160,17 @@ class PackingThread(QThread): cmd.append(script_posix) - success = self.run_cmd(cmd, cwd=Path.cwd().resolve().as_posix()) + success = self.run_cmd(cmd, cwd=script_dir.as_posix()) if self._is_cancelled: return self.finished.emit(False, "[INFO] 构建已被终止。") self.progress.emit("[INFO] 编译核心完成,正在提取并归档编译产物...") - cwd = Path.cwd().resolve() src_out = None if engine == "PyInstaller": if sys.platform == "darwin" and self.params['noconsole']: - src_out = cwd / "dist" / f"{app_name}.app" + src_out = self.temp_dist_dir / f"{app_name}.app" else: - src_out = cwd / "dist" / (f"{app_name}{ext}" if self.params['onefile'] else app_name) + 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']: @@ -2263,27 +2262,20 @@ class PackingThread(QThread): except: pass if self.params['clean_all']: - self.progress.emit("[INFO] 正在释放空间,删除虚拟环境及临时编译缓存...") + self.progress.emit("[INFO] 正在释放空间,安全清除临时构建环境...") - for p in [self.venv_dir, self.temp_workpath, self.temp_out_dir]: + for p in [self.venv_dir, self.temp_workpath, self.temp_out_dir, self.temp_dist_dir]: if p and p.exists(): robust_rmtree(p) - cwd = Path.cwd().resolve() app_name = self.params.get('app_name', 'app') - robust_rmtree(cwd / "dist") - for p in ["build", "__pycache__", f"{app_name}.build", f"{app_name}.dist", f"{app_name}.onefile-build"]: - robust_rmtree(cwd / p) + for p in ["__pycache__", f"{app_name}.build", f"{app_name}.dist", f"{app_name}.onefile-build"]: + robust_rmtree(script_dir / p) - spec_file = cwd / f"{app_name}.spec" + spec_file = script_dir / f"{app_name}.spec" if spec_file.exists(): try: spec_file.unlink() except: pass - - crash_xml = cwd / "nuitka-crash-report.xml" - if crash_xml.exists(): - try: crash_xml.unlink() - except: pass class MainWindow(QMainWindow): def __init__(self): @@ -2713,12 +2705,10 @@ class MainWindow(QMainWindow): self.update_ui_state("failed") def open_dist(self): - if self.settings_panel.out_mode_combo.currentIndex() == 1: + if self.settings_panel.out_mode_combo.currentIndex() == 1 and self.settings_panel.out_dir_edit.text().strip(): target = Path(self.settings_panel.out_dir_edit.text().strip()) - elif self.settings_panel.clean_all_check.isChecked() and self.script_path: - target = Path(self.script_path).parent - else: - target = Path.cwd() / "dist" + else: + target = Path(self.script_path).parent if self.script_path else Path.cwd() if target.exists(): try: