v2.3.6: Script fixes and version bump
This commit is contained in:
1 parent
60173c2ec4
commit
c4ba84a603
4 files changed
+201
-56
No files matched your search
@@ -0,0 +1,46 @@
|
||||
name: Build and Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*' # 当推送形如 v1.0.0 或 v2.3.6 的标签时触发
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish Python distribution
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install build
|
||||
|
||||
- name: Build sdist and wheel
|
||||
run: python -m build
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
dist/*.whl
|
||||
dist/*.tar.gz
|
||||
generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
skip-existing: true
|
||||
@@ -13,12 +13,12 @@ QPyPack 是一个为 Python 开发者设计的图形化打包配置工具。它
|
||||
为了解决传统命令行打包参数多、依赖易冲突、配置繁琐的问题,QPyPack 围绕“易用性”与“官方最佳实践”进行了深度设计:
|
||||
|
||||
### 1. 极简的交互流程
|
||||
* **拖拽即装载**:无需手动输入路径,直接将您的 `.py` 源代码文件拖入软件窗口,即可自动识别并准备构建。
|
||||
* **智能图标匹配**:脚本载入后,程序会自动检索同目录下的常用图标文件(如 `icon.ico` / `logo.ico`)并自动填充和预览。
|
||||
* **拖拽即装载**:无需手动输入路径,直接将您的 `.py` 或 `.pyw` 源代码文件拖入软件窗口,即可自动识别并准备构建。
|
||||
* **智能图标匹配**:脚本载入后,程序会自动检索同目录下的常用图标文件(如 `icon.ico` / `logo.ico` / `icon.svg` / `logo.svg`)并自动填充和预览。
|
||||
* **一键直达结果**:打包完成后,点击“打开输出目录”按钮即可自动弹出资源管理器并定位到生成的程序。
|
||||
|
||||
### 2. 自动化的依赖与环境处理
|
||||
* **智能沙盒隔离 (Venv)**:支持一键开启虚拟环境。工具会在临时目录下自动创建干净的沙盒环境,仅安装打包所需的库,有效减小最终生成的 `.exe` 文件的体积。
|
||||
* **智能沙盒隔离 (Venv)**:支持一键开启虚拟环境。工具会在临时目录下自动创建干净的沙盒环境,仅安装打包所需的库,有效减小最终生成的可执行文件的体积。
|
||||
* **多维度依赖扫描**:
|
||||
* 自动检测并优先读取项目目录下的 `requirements.txt`。
|
||||
* 提供辅助的 AST(静态语法树)扫描选项,自动分析代码中导入的第三方模块,帮助您查漏补缺隐式依赖。
|
||||
@@ -26,19 +26,25 @@ QPyPack 是一个为 Python 开发者设计的图形化打包配置工具。它
|
||||
|
||||
### 3. 可视化高级参数配置
|
||||
* **双引擎无缝切换**:在下拉菜单中即可在 PyInstaller 和 Nuitka 之间灵活切换,相关的参数输入框与高级选项会根据所选引擎特性自动启用或隐藏。
|
||||
* **可视化版本属性**:无需编写复杂的版本配置文件,直接在界面文本框中输入版本号、公司名和产品描述,工具会自动将其写入生成的 `.exe` 元数据中,提升系统识别度。
|
||||
* **可视化版本属性**:无需编写复杂的版本配置文件,直接在界面文本框中输入版本号、公司名和产品描述,工具会自动将其写入生成的可执行文件元数据中,提升系统识别度。
|
||||
* **直观的资源添加**:通过交互式对话框选择需要打包的图片、数据文件夹,引擎会自动进行跨平台兼容的路径映射与资源封装。
|
||||
|
||||
---
|
||||
### Changelog
|
||||
**2.3.6**
|
||||
* **新增 SVG 格式图标支持**:现在能够自动加载 `icon.svg` / `logo.svg`为程序图标。
|
||||
* **增加 `.pyw` 格式脚本支持**:全面支持无控制台窗口的 Python 脚本格式,优化拖拽校验与文件选择过滤器。
|
||||
* **修复 Windows 绝对路径分割 BUG**:重构附加资源解析器,避免绝对路径中的盘符冒号(如 `C:`)被错误截断,确保跨引擎数据装载安全。
|
||||
* **优化 AST 本地依赖过滤**:安装前主动检索当前目录结构,自动过滤本地辅助模块或文件夹,防止向 PyPI 发起不必要的安装请求。
|
||||
* **强化元数据清理机制**:将临时版本配置文件的生命周期管理提升至最高级别,确保即使在未启用“一键清理”时,构建结束也会无条件收回临时文件。
|
||||
|
||||
**2.3.5**
|
||||
* **防漏包机制**:彻底废弃极易误判的短路判定,重构依赖拉取架构为“三重绝对安全网”(Requirements -> Pipreqs -> AST源码强扫)。即使前置分析工具漏报,最终的 AST 保底扫描也会强行抓取 `PySide6` 等核心隐式库并塞入沙盒,彻底终结漏包闪退问题。
|
||||
* **防漏包机制**:彻底废弃极易误判的短路判定,重构依赖拉取架构为“三重绝对安全网”(Requirements -> Pipreqs -> AST源码强扫)。即使前置分析工具漏报,最终的 AST 保底扫描也会强行抓取 `PySide6` 等核心隐式库并塞入沙盒,终结漏包闪退问题。
|
||||
* **工作区“零污染”改造**:贯彻绝对纯净的构建理念。将 `QPyPack.ini` 配置文件平滑迁移至全局系统路径(`~/.qpypack/config.ini`),并将所有 AST 缓存、依赖清单等中间产物全部转移至操作系统 `Temp` 目录,确保不留任何垃圾文件污染用户的工程代码区。
|
||||
* **构建日志工程化规范**:全面重构控制台与 UI 日志的输出文案,引入规范化的 CI/CD 生命周期状态标签(如 `[Init]`, `[Env]`, `[Deps]`, `[Build]`, `[Pack]`),大幅提升状态诊断的清晰度与专业性。
|
||||
* **虚拟环境沙盒智能自愈**:在分配独立隔离的 venv 打包沙盒后,首个初始化动作变更为静默自动升级 `pip`,彻底压制并清除了因老旧模块引发的大段黄色警告乱码。
|
||||
* **核心 BUG 修复**:精准修复了因底层 `configparser` 映射字典降级读取失败,导致抛出 `'dict' object has no attribute 'lower'` 的致命性系统崩溃问题。
|
||||
* **优化日志表述**:调整了日志输出文字的语言表述,现在更加专业化。
|
||||
* **虚拟环境沙盒智能自愈**:在分配独立隔离的 venv 打包沙盒后,首个初始化动作变更为静默自动升级 `pip`,清除因老旧模块引发的大段黄色警告乱码。
|
||||
* **核心 BUG 修复**:精准修复了因底层 `configparser` 映射字典降级读取失败,导致抛出 `'dict' object has no attribute 'lower'` 的系统崩溃问题。
|
||||
* **优化日志表述**:调整了日志输出文字的语言表述,使之更具工程专业度。
|
||||
|
||||
**2.3.0**
|
||||
* **UI框架重构**:由 PyQt5 升级至 PySide6。优化了现代高分屏(High-DPI)下的文字与图标缩放表现。
|
||||
@@ -46,16 +52,16 @@ QPyPack 是一个为 Python 开发者设计的图形化打包配置工具。它
|
||||
* **Nuitka 官方最佳实践深度集成**:
|
||||
* 单文件 (`--onefile`) 模式下,强制启用 `zstandard` 核心压缩环境,大幅优化产物体积与解压启动性能。
|
||||
* 全局引入 `anti-bloat` 防膨胀插件,智能拦截并避免 `pytest`、`IPython` 等无用巨大依赖被误打包。
|
||||
* 新增对 `tkinter` 官方探针的自动识别与插件激活。
|
||||
* 新增对 `tkinter` 官方探针 of 自动识别与插件激活。
|
||||
* 重构附加资源打包逻辑,引擎将自动探测目标类型,严格区分并适配单文件 (`--include-data-files`) 与文件夹 (`--include-data-dir`) 打包规范。
|
||||
* **PyInstaller 深度优化**:
|
||||
* 移除过时的强制 PyQt 隐藏导入逻辑,交由现代 Hook 自动完美处理。
|
||||
* 移除过时的强制 PyQt 隐藏导入逻辑,交由现代 Hook 自动处理。
|
||||
* 彻底修复附加资源在跨平台构建时的路径分隔符 (`os.pathsep`) 匹配与切割异常问题。
|
||||
|
||||
**2.2.0**
|
||||
* 更名为 QPyPack,规范目录结构 for PyPI,优化内部沙盒与临时文件命名规范。
|
||||
* 超时守护 (Watchdog):引入看门狗机制,为关键打包步骤设置超时限制,防止因网络问题导致构建无限期挂起。
|
||||
* 依赖推导优化:pipreqs 原生支持国内镜像站重定向加速,并增加 15 秒超时自愈,失败时自动降级为本地纯静态 AST 解析。
|
||||
* 依赖推推导优化:pipreqs 原生支持国内镜像站重定向加速,并增加 15 秒超时自愈,失败时自动降级为本地纯静态 AST 解析。
|
||||
* 智能命名输出:自动解析脚本版本,预设输出文件名为 `{Name}_{Version}` 格式(例如:QPyPack_2.2.0.exe)。
|
||||
* 编译兼容扩展:Nuitka 编译模式新增对 PyQt6、PySide、NumPy、Matplotlib 等常用插件的自适应识别。
|
||||
* 细节打磨:改进日志高频刷新滚动体验,优化版本信息元数据抓取退级逻辑。
|
||||
@@ -76,7 +82,7 @@ QPyPack 是一个为 Python 开发者设计的图形化打包配置工具。它
|
||||
|
||||
只需三步,即可完成打包:
|
||||
|
||||
1. **拖入脚本**:将您的 Python 脚本拖拽至主界面。
|
||||
1. **拖入脚本**:将您的 Python 脚本(`.py` 或 `.pyw`)拖拽至主界面。
|
||||
2. **选择引擎**:点击右下角设置,在“基础打包”中选择您想使用的引擎(推荐默认的 PyInstaller 进行快速验证)。
|
||||
3. **开始构建**:返回主界面点击“开始构建”,并在下方展开的控制台中实时查看打包进度。
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "qpypack"
|
||||
version = "2.3.5"
|
||||
version = "2.3.6"
|
||||
description = "A modern graphical packaging config tool for PyInstaller, Nuitka."
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
|
||||
+136
-43
@@ -1,12 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__app_name__ = "QPyPack"
|
||||
__version__ = "2.3.6"
|
||||
__author__ = "QwejayHuang"
|
||||
__company__ = "QwejayHuang"
|
||||
__description__ = "自动化 Python 脚本打包构建工具"
|
||||
|
||||
import sys
|
||||
import os
|
||||
import shutil
|
||||
@@ -39,6 +30,12 @@ from PySide6.QtCore import Qt, QThread, Signal, QPropertyAnimation, QEasingCurve
|
||||
from PySide6.QtGui import QFont, QDragEnterEvent, QDropEvent, QTextCursor, QIcon, QPixmap, QPainter, QColor, QPen
|
||||
from PySide6.QtSvg import QSvgRenderer
|
||||
|
||||
__app_name__ = "QPyPack"
|
||||
__version__ = "2.3.6"
|
||||
__author__ = "QwejayHuang"
|
||||
__company__ = "QwejayHuang"
|
||||
__description__ = "自动化 Python 脚本打包构建工具"
|
||||
|
||||
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',
|
||||
@@ -96,12 +93,15 @@ def is_cloud_locked(filepath):
|
||||
try:
|
||||
with open(filepath, 'rb') as f:
|
||||
return b"__CLOUDSYNC_ENC__" in f.read(1024)
|
||||
except:
|
||||
except (PermissionError, OSError):
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def extract_imports_via_ast(script_path, python_exe):
|
||||
code_snippet = (
|
||||
"import ast, sys, json, re\n"
|
||||
"code = ''\n"
|
||||
"try:\n"
|
||||
" with open(sys.argv[1], 'rb') as f: raw = f.read()\n"
|
||||
" code = raw.decode('utf-8-sig') if raw.startswith(b'\\xef\\xbb\\xbf') else raw.decode('utf-8', errors='ignore')\n"
|
||||
@@ -290,6 +290,25 @@ def robust_rmtree(path: Path, retries=15, delay=0.8):
|
||||
except: time.sleep(delay)
|
||||
return False
|
||||
|
||||
def parse_add_data(add_data_str):
|
||||
datas = []
|
||||
for d in add_data_str.split(','):
|
||||
d = d.strip()
|
||||
if not d: continue
|
||||
if ":" in d:
|
||||
last_colon_idx = d.rfind(':')
|
||||
if last_colon_idx > 1:
|
||||
src_path = d[:last_colon_idx].strip()
|
||||
dst_path = d[last_colon_idx+1:].strip()
|
||||
else:
|
||||
src_path = d.strip()
|
||||
dst_path = "."
|
||||
else:
|
||||
src_path = d.strip()
|
||||
dst_path = "."
|
||||
datas.append((src_path, dst_path))
|
||||
return datas
|
||||
|
||||
|
||||
class AnimatedButton(QPushButton):
|
||||
def __init__(self, text, parent=None):
|
||||
@@ -331,6 +350,7 @@ class AnimatedButton(QPushButton):
|
||||
self.animation_group.start()
|
||||
super().leaveEvent(event)
|
||||
|
||||
|
||||
class TargetIconWidget(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -568,12 +588,6 @@ class DropArea(QFrame):
|
||||
|
||||
layout.addStretch(1)
|
||||
|
||||
def reset(self):
|
||||
self.icon_widget.reset()
|
||||
self.label.setText("将 Python 脚本 (.py/.pyw) 拖拽至此处\n或 点击浏览文件")
|
||||
self.label.setStyleSheet("QLabel { background: transparent; color: #5F6368; font-size: 16px; font-weight: bold; border: none; }")
|
||||
self.sub_label.setText("智能解析工程依赖、静态附件及隐藏模块树")
|
||||
|
||||
def dragEnterEvent(self, event: QDragEnterEvent):
|
||||
if event.mimeData().hasUrls():
|
||||
file_path = event.mimeData().urls()[0].toLocalFile().lower()
|
||||
@@ -654,9 +668,14 @@ class DropArea(QFrame):
|
||||
self.label.setText("构建任务异常中断!")
|
||||
self.label.setStyleSheet("QLabel { background: transparent; color: #D93025; font-size: 20px; font-weight: bold; border: none; }")
|
||||
self.sub_label.setText("请查阅下方的执行日志报告以进行问题排查")
|
||||
|
||||
def reset(self):
|
||||
self.icon_widget.reset()
|
||||
self.label.setText("将 Python 脚本 (.py/.pyw) 拖拽至此处\n或 点击浏览文件")
|
||||
self.label.setStyleSheet("QLabel { background: transparent; color: #5F6368; font-size: 16px; font-weight: bold; border: none; }")
|
||||
self.sub_label.setText("智能解析工程依赖、静态附件及隐藏模块树")
|
||||
|
||||
|
||||
# ======================== 设置面板 ========================
|
||||
class SettingsPanel(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -970,7 +989,7 @@ class SettingsPanel(QWidget):
|
||||
self.icon_preview.clear()
|
||||
|
||||
def select_icon(self):
|
||||
p, _ = QFileDialog.getOpenFileName(self, "选择图标", "", "Icon Files (*.ico)")
|
||||
p, _ = QFileDialog.getOpenFileName(self, "选择图标", "", "Icon Files (*.ico *.svg)")
|
||||
if p: self.icon_edit.setText(Path(p).resolve().as_posix())
|
||||
|
||||
def auto_scan_hidden(self):
|
||||
@@ -1012,7 +1031,6 @@ class SettingsPanel(QWidget):
|
||||
self.add_data_edit.setText(f"{curr}, {folder}:{dest}" if curr else f"{folder}:{dest}")
|
||||
|
||||
|
||||
# ======================== 核心构建引擎 ========================
|
||||
class PackingThread(QThread):
|
||||
progress = Signal(str)
|
||||
finished = Signal(bool, str)
|
||||
@@ -1104,6 +1122,34 @@ class PackingThread(QThread):
|
||||
def sanitize_script(self, orig_path: Path):
|
||||
if is_cloud_locked(orig_path):
|
||||
return None, False, "目标脚本处于云盘加密或锁定状态,请解密后重试。"
|
||||
|
||||
if not self.params['noconsole']:
|
||||
try:
|
||||
raw = orig_path.read_bytes()
|
||||
try: code = raw.decode('utf-8-sig')
|
||||
except: code = raw.decode(locale.getpreferredencoding(), errors='ignore')
|
||||
|
||||
pause_code = "\n" + "#"*30 + "\n" + (
|
||||
"try:\n"
|
||||
" import sys\n"
|
||||
" if sys.platform == 'win32':\n"
|
||||
" import ctypes\n"
|
||||
" kernel32 = ctypes.windll.kernel32\n"
|
||||
" process_list = (ctypes.c_uint * 10)()\n"
|
||||
" num_processes = kernel32.GetConsoleProcessList(process_list, 10)\n"
|
||||
" if num_processes <= 2:\n"
|
||||
" input('\\n执行完毕,按回车键退出...')\n"
|
||||
"except:\n"
|
||||
" pass\n"
|
||||
)
|
||||
|
||||
temp_dir = Path(tempfile.gettempdir())
|
||||
temp_file = temp_dir / f"qpypack_build_target_{int(time.time())}.py"
|
||||
temp_file.write_text(code + pause_code, encoding='utf-8')
|
||||
return temp_file, True, ""
|
||||
except Exception as e:
|
||||
self.progress.emit(f"[Warn] 注入控制台防闪退机制失败: {e}")
|
||||
|
||||
return orig_path, False, ""
|
||||
|
||||
def run(self):
|
||||
@@ -1202,7 +1248,12 @@ class PackingThread(QThread):
|
||||
for k, v in config['Mappings'].items():
|
||||
known_mappings[k] = v
|
||||
|
||||
ast_pkgs = [known_mappings.get(m, m) for m in script_imports if m not in STD_LIBS]
|
||||
local_files = {p.stem.lower() for p in script_dir.glob("*")}
|
||||
ast_pkgs = [
|
||||
known_mappings.get(m, m)
|
||||
for m in script_imports
|
||||
if m not in STD_LIBS and m.lower() not in local_files
|
||||
]
|
||||
|
||||
if ast_pkgs:
|
||||
self.progress.emit(f"[Deps] 阶段 3/3: 启动 AST 源码扫描...")
|
||||
@@ -1239,12 +1290,9 @@ class PackingThread(QThread):
|
||||
|
||||
for imp in self.params.get('hidden_imports', '').split(','):
|
||||
if imp.strip(): cmd.extend(["--hidden-import", imp.strip()])
|
||||
for d in self.params.get('add_data', '').split(','):
|
||||
d = d.strip()
|
||||
if d:
|
||||
parts = d.rsplit(':', 1)
|
||||
if len(parts) == 2:
|
||||
cmd.extend(["--add-data", f"{parts[0].strip()}{os.pathsep}{parts[1].strip()}"])
|
||||
|
||||
for src, dst in parse_add_data(self.params.get('add_data', '')):
|
||||
cmd.extend(["--add-data", f"{src}{os.pathsep}{dst}"])
|
||||
|
||||
elif engine == "Nuitka":
|
||||
self.temp_out_dir = Path(tempfile.mkdtemp(prefix="nuitka_out_")).resolve()
|
||||
@@ -1284,17 +1332,12 @@ class PackingThread(QThread):
|
||||
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:
|
||||
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}")
|
||||
for src, dst in parse_add_data(self.params.get('add_data', '')):
|
||||
src_path = Path(src)
|
||||
if src_path.is_dir():
|
||||
cmd.append(f"--include-data-dir={src_path.resolve().as_posix()}={dst}")
|
||||
else:
|
||||
cmd.append(f"--include-data-files={src_path.resolve().as_posix()}={dst}")
|
||||
|
||||
cmd.append(script_posix)
|
||||
|
||||
@@ -1318,7 +1361,7 @@ class PackingThread(QThread):
|
||||
|
||||
if success and final_out.exists():
|
||||
self.progress.emit("[Pack] 正在校验最终程序完整性...")
|
||||
self.finished.emit(True, f"[Success] 构建结束!\n==> 输出路径: {final_out.resolve().as_posix()}")
|
||||
self.finished.emit(True, f"[Success] 构建结束!路径: {final_out.resolve().as_posix()}")
|
||||
else:
|
||||
self.finished.emit(False, "[Failed] 构建任务异常中断,请参阅上方运行日志以排查错误。")
|
||||
except Exception as e:
|
||||
@@ -1328,6 +1371,14 @@ class PackingThread(QThread):
|
||||
try: build_script_path.unlink()
|
||||
except: pass
|
||||
|
||||
if self.params.get('version_file'):
|
||||
try: Path(self.params['version_file']).unlink(missing_ok=True)
|
||||
except: pass
|
||||
|
||||
if self.params.get('temp_icon_file'):
|
||||
try: Path(self.params['temp_icon_file']).unlink(missing_ok=True)
|
||||
except: pass
|
||||
|
||||
if self.params['clean_all']:
|
||||
self.progress.emit("[Clean] 正在释放工作区,抹除虚拟环境与沙盒临时数据...")
|
||||
for p in [self.venv_dir, self.temp_workpath, self.temp_out_dir]:
|
||||
@@ -1339,10 +1390,8 @@ class PackingThread(QThread):
|
||||
for p in ["build", "__pycache__", f"{app_name}.build", f"{app_name}.dist", f"{app_name}.onefile-build"]:
|
||||
robust_rmtree(cwd / p)
|
||||
Path(cwd / f"{app_name}.spec").unlink(missing_ok=True)
|
||||
if self.params.get('version_file'): Path(self.params['version_file']).unlink(missing_ok=True)
|
||||
|
||||
|
||||
# ======================== 界面主窗口 ========================
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -1545,6 +1594,7 @@ class MainWindow(QMainWindow):
|
||||
author = "My Studio"
|
||||
desc = "Python Executable"
|
||||
|
||||
script_imports = set()
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
content = f.read(10240)
|
||||
@@ -1573,6 +1623,14 @@ class MainWindow(QMainWindow):
|
||||
|
||||
except: pass
|
||||
|
||||
try:
|
||||
script_imports = extract_imports_via_ast(path, get_python_executable())
|
||||
except: pass
|
||||
|
||||
gui_libs = {'pyqt5', 'pyqt6', 'pyside2', 'pyside6', 'tkinter', 'wx', 'kivy', 'libavg'}
|
||||
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)
|
||||
|
||||
default_output_name = f"{app_name}_{version}" if version else app_name
|
||||
self.settings_panel.name_edit.setText(default_output_name)
|
||||
|
||||
@@ -1590,11 +1648,15 @@ class MainWindow(QMainWindow):
|
||||
break
|
||||
|
||||
self.drop_area.set_success(Path(path).name, custom_icon_path=auto_icon)
|
||||
self.status_label.setText(f" 状态: 已锁定源文件 {Path(path).name}")
|
||||
|
||||
status_suffix = ",已配置为【保留控制台】模式" if not has_gui else ",已配置为GUI模式"
|
||||
self.status_label.setText(f" 状态: 已锁定源文件 {Path(path).name}{status_suffix}")
|
||||
|
||||
if not self.log_container.isVisible(): self.toggle_log()
|
||||
self.log.clear()
|
||||
self.append_log(f"[System] 源文件绑定成功: {path}")
|
||||
if not has_gui:
|
||||
self.append_log("[System] 检测到该脚本不包含常见 GUI 图形框架。已为您保留控制台。")
|
||||
self.update_ui_state("ready")
|
||||
|
||||
def cancel_pack(self):
|
||||
@@ -1625,13 +1687,43 @@ class MainWindow(QMainWindow):
|
||||
version_file.write_text(content, encoding='utf-8')
|
||||
except: pass
|
||||
|
||||
icon_path_str = sp.icon_edit.text().strip()
|
||||
temp_icon_file = None
|
||||
if icon_path_str and Path(icon_path_str).exists():
|
||||
icon_path = Path(icon_path_str)
|
||||
if icon_path.suffix.lower() == '.svg':
|
||||
try:
|
||||
renderer = QSvgRenderer()
|
||||
if renderer.load(icon_path.as_posix()):
|
||||
pixmap = QPixmap(256, 256)
|
||||
pixmap.fill(Qt.GlobalColor.transparent)
|
||||
painter = QPainter(pixmap)
|
||||
renderer.render(painter)
|
||||
painter.end()
|
||||
|
||||
temp_ico = Path(tempfile.gettempdir()) / f"qpypack_temp_icon_{int(time.time())}.ico"
|
||||
if pixmap.save(temp_ico.as_posix(), "ICO"):
|
||||
icon_path_str = temp_ico.as_posix()
|
||||
temp_icon_file = temp_ico.as_posix()
|
||||
else:
|
||||
temp_png = Path(tempfile.gettempdir()) / f"qpypack_temp_icon_{int(time.time())}.png"
|
||||
if pixmap.save(temp_png.as_posix(), "PNG"):
|
||||
icon_path_str = temp_png.as_posix()
|
||||
temp_icon_file = temp_png.as_posix()
|
||||
except Exception as e:
|
||||
self.append_log(f"[Warn] SVG 图标解析异常: {e}")
|
||||
|
||||
if not temp_icon_file:
|
||||
icon_path_str = None
|
||||
self.append_log("[Warn] SVG 桌面图标转换失败,已自动降级为无图标模式构建以确保成功。")
|
||||
|
||||
params = {
|
||||
'engine': engine,
|
||||
'script_path': self.script_path,
|
||||
'app_name': app_name,
|
||||
'onefile': sp.onefile_check.isChecked(),
|
||||
'noconsole': sp.noconsole_check.isChecked(),
|
||||
'icon': sp.icon_edit.text().strip(),
|
||||
'icon': icon_path_str,
|
||||
'use_reqs': sp.reqs_check.isChecked(),
|
||||
'use_pipreqs': sp.pipreqs_check.isChecked(),
|
||||
'hidden_imports': sp.hidden_edit.text(),
|
||||
@@ -1640,6 +1732,7 @@ 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,
|
||||
'temp_icon_file': temp_icon_file,
|
||||
'ver_comp': sp.ver_comp.text(),
|
||||
'ver_desc': sp.ver_desc.text(),
|
||||
'ver_ver': sp.ver_ver.text(),
|
||||
@@ -1708,4 +1801,4 @@ def main():
|
||||
sys.exit(app.exec())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
Reference in new issue
Block a user