From 2c02d9db73f67e483bc3005e0df4b1c6e1a90f82 Mon Sep 17 00:00:00 2001 From: QwejayHuang Date: Fri, 24 Jul 2026 16:39:54 +0800 Subject: [PATCH] v2.6.1: Minor fixes and stability updates --- README.md | 36 ++++++++++++++++++++----------- pyproject.toml | 2 +- qpypack/main.py | 56 ++++++++++++++++++++++++++++++++++++------------- 3 files changed, 66 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 33222dd..a4abf4f 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,9 @@ QPyPack 是一款致力于简化 Python 应用打包流程的可视化工具。 ## 📷 界面预览 (Screenshots)

-image -image +image +image +

--- @@ -79,6 +80,12 @@ qpypack ## 📅 更新日志 (Changelog) +### [2.6.1] - 2026-07-25 + +* **消除依赖污染**:修复了开启“精简模式”时,AST 引擎错误遍历整个父目录的 Bug,解决打包产物被混入无关依赖的问题。 +* **构建沙盒隔离**:默认将目标脚本放入独立沙盒中解析,杜绝同目录其他脚本的依赖干扰;同时在 UI 界面新增“允许扫描同目录所有文件”开关。 +* **MSVC 智能检测**:引入本地 C++ 编译环境智能嗅探。若系统已安装 MSVC,Nuitka 将自动启用 --msvc=latest,优先使用系统原生编译器,提升兼容性并免除耗时的 MinGW 下载。 +* **UPX 警告静默**:自动识别并跳过系统核心动态库的 UPX 压缩报错,并小幅提升了构建速度。 ### [2.6.0] - 2026-07-24 @@ -208,18 +215,23 @@ qpypack --- -## 📄 开源协议 (License) +## 💖 赞助支持 (Sponsorship) -本项目基于 **GPL-3.0** 开源许可协议。在遵守该开源许可协议的前提下,您可以自由分发、修改和二次开发。 +QPyPack 是一个完全开源且免费的项目,由个人利用业余时间开发与维护。如果本项目帮助您提高了开发效率或解决了打包难题,欢迎通过以下方式对项目进行**自愿赞助**。您的支持将是本项目持续更新与维护的重要动力: + +- ⚡ **请作者喝咖啡**:[赞助支持 QPyPack](https://www.ifdian.net/a/qwejay)(支持微信 / 支付宝) + +> **赞助说明**:赞助完全出于自愿,属于对开源项目的无偿鼓励,不包含任何商业服务绑定或特定功能开发承诺。非常感谢每一位支持开源创作的朋友! --- -## 📈 Star History +## 📄 开源协议 (License) - - - - - Star History Chart - - +本项目基于 [GNU General Public License v3.0](LICENSE) 开源,允许在遵循协议条款的前提下自由分发、修改和二次开发。 + +> [!NOTE] +> **关于打包产物的版权**: +> 使用 QPyPack 构建生成的二进制文件/应用程序,其版权和开源许可**完全由使用者自行决定**,QPyPack 的 GPL-3.0 协议不会影响用户打包后的程序。 + +Copyright (C) 2026 QwejayHuang. +``` diff --git a/pyproject.toml b/pyproject.toml index 66256e1..47b32dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "qpypack" -version = "2.6.0" +version = "2.6.1" 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 f9f9cef..d7a4846 100644 --- a/qpypack/main.py +++ b/qpypack/main.py @@ -47,7 +47,7 @@ except ImportError: HAS_QT_AUDIO = False __app_name__ = "QPyPack" -__version__ = "2.6.0" +__version__ = "2.6.1" __author__ = "QwejayHuang" __company__ = "QwejayHuang" __description__ = "基于 PyInstaller 与 Nuitka 的跨平台 Python 应用打包构建工具" @@ -150,7 +150,7 @@ def load_config(): 'pip_index_backup': 'https://mirrors.aliyun.com/pypi/simple/', 'onefile': 'True', 'noconsole': 'True', 'clean_all': 'True', 'auto_icon': 'True', 'use_venv': 'True', 'use_reqs': 'True', - 'use_pipreqs': 'True', 'upx': 'False', 'concise_log': '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': '', 'sound_notify': 'True', 'auto_save_log': 'False', @@ -1695,9 +1695,11 @@ class SettingsPanel(QWidget): self.venv_check = QCheckBox("启用独立虚拟环境打包 (推荐)") self.reqs_check = QCheckBox("同步安装 requirements.txt 声明") self.pipreqs_check = QCheckBox("自动分析并补全项目依赖 (pipreqs)") + self.pipreqs_dir_check = QCheckBox("允许 pipreqs 扫描同目录所有文件") g_dep.addWidget(self.venv_check, 0, 0) g_dep.addWidget(self.reqs_check, 0, 1) - g_dep.addWidget(self.pipreqs_check, 1, 0, 1, 2) + g_dep.addWidget(self.pipreqs_check, 1, 0) + g_dep.addWidget(self.pipreqs_dir_check, 1, 1) c_lay_deps.addLayout(g_dep) lay_sub2.addWidget(card_deps) @@ -2002,6 +2004,7 @@ class SettingsPanel(QWidget): "use_venv": self.venv_check.isChecked(), "use_reqs": self.reqs_check.isChecked(), "use_pipreqs": self.pipreqs_check.isChecked(), + "use_pipreqs_dir": self.pipreqs_dir_check.isChecked(), "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), @@ -2031,6 +2034,7 @@ class SettingsPanel(QWidget): if "use_venv" in data: self.venv_check.setChecked(bool(data["use_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"]) @@ -2063,6 +2067,7 @@ class SettingsPanel(QWidget): self.venv_check.setChecked(s.getboolean('use_venv', True)) 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', '')) @@ -2112,6 +2117,7 @@ class SettingsPanel(QWidget): s['use_venv'] = str(self.venv_check.isChecked()) 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() @@ -2405,6 +2411,8 @@ class PackingThread(QThread): l_lower = l.lower() if "error:" in l_lower: return False + if "upx" in l_lower and ("subprocess.calledprocesserror" in l_lower or "notcompressibleexception" in l_lower): + return True return any(kw in l_lower for kw in ["warning:", "info:", "deprecation:", "userwarning:", "futurewarning:"]) for line in self.process.stdout: @@ -2574,16 +2582,6 @@ class PackingThread(QThread): script_imports = set() try: script_imports = extract_imports_via_ast(script_posix, system_python_exe) - if self.params.get('lite_mode'): - scan_count = 0 - for py_file in script_dir.rglob("*.py"): - if any(p.startswith('.') or p.lower() in ('venv', 'env', 'site-packages', 'node_modules', '__pycache__') for p in py_file.parts): - continue - if scan_count > 500: break - try: - script_imports.update(extract_imports_via_ast(py_file.as_posix(), system_python_exe)) - scan_count += 1 - except: pass except Exception as e: self.progress.emit(f"[WARN] 源码 AST 分析异常: {e}") @@ -2639,6 +2637,16 @@ class PackingThread(QThread): if self.params.get('use_pipreqs'): self.progress.emit("[INFO] 依赖安装 [2/3]: 正在调用 pipreqs 分析项目依赖...") + sandbox_dir = None + if not self.params.get('use_pipreqs_dir', False): + sandbox_dir = Path(tempfile.mkdtemp(prefix="qpypack_sandbox_")).resolve() + shutil.copy2(build_script_path, sandbox_dir / build_script_path.name) + target_scan_dir = sandbox_dir + self.progress.emit("[INFO] 已启用单文件沙盒模式:仅对当前脚本解析,防止同目录其他文件污染。") + else: + target_scan_dir = script_dir + self.progress.emit("[WARN] 已允许全目录扫描模式:正在扫描当前目录下所有 Python 文件...") + pipreqs_pkg = "pipreqs" if self.params.get('pipreqs_version'): pipreqs_pkg = f"pipreqs=={self.params['pipreqs_version']}" @@ -2651,7 +2659,7 @@ class PackingThread(QThread): pypi_server = re.sub(r'/simple/?$', '/pypi', pip_idx, flags=re.I).rstrip('/') pipreqs_cmd = [ - python_exe, "-m", "pipreqs.pipreqs", script_dir.as_posix(), + python_exe, "-m", "pipreqs.pipreqs", target_scan_dir.as_posix(), "--encoding", "utf-8", "--force", "--savepath", temp_pipreqs.as_posix() ] if pypi_server: @@ -2677,6 +2685,9 @@ class PackingThread(QThread): if success_pipreqs and temp_pipreqs.exists(): self.run_pip_install(python_exe, ["-q", "-r", temp_pipreqs.as_posix()]) temp_pipreqs.unlink(missing_ok=True) + + if sandbox_dir and sandbox_dir.exists(): + robust_rmtree(sandbox_dir) config = load_config() known_mappings = DEFAULT_MAPPINGS.copy() @@ -2772,6 +2783,8 @@ class PackingThread(QThread): else: upx_dir_default = (Path.cwd() / "upx").resolve() if upx_dir_default.exists(): cmd.append(f"--upx-dir={upx_dir_default.as_posix()}") + if os.name == "nt": + cmd.extend(["--upx-exclude=python3.dll", "--upx-exclude=python*.dll", "--upx-exclude=vcruntime140.dll"]) else: cmd.append("--noupx") @@ -2794,6 +2807,18 @@ class PackingThread(QThread): f"--output-filename={app_name}{ext}" ] + if os.name == "nt": + has_msvc = shutil.which('cl.exe') is not None + if not has_msvc: + vswhere = Path(os.environ.get("ProgramFiles(x86)", "C:\\Program Files (x86)")) / "Microsoft Visual Studio/Installer/vswhere.exe" + if vswhere.exists(): + try: + res = subprocess.run([vswhere.as_posix(), "-latest", "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64"], capture_output=True, text=True, creationflags=subprocess.CREATE_NO_WINDOW) + if res.stdout.strip(): has_msvc = True + except: pass + if has_msvc: + cmd.append("--msvc=latest") + self.progress.emit("[INFO] 发现本地 MSVC 环境,优先使用原生 C++ 编译器。") cores = self.params.get('cpu_cores', os.cpu_count() or 2) cmd.append(f"--jobs={cores}") @@ -3375,6 +3400,7 @@ class MainWindow(QMainWindow): '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, @@ -3576,4 +3602,4 @@ def main(): sys.exit(app.exec()) if __name__ == "__main__": - main() \ No newline at end of file + main()