diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 1ec1165..cd9d821 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -10,9 +10,9 @@ on:
# =================================================================
env:
PROJECT_NAME: "qpypack" # 项目包名
- MAIN_SCRIPT: "qpypack/main.py" # Python入口文件
- ICON_PATH: "qpypack/icon.ico" # 应用程序图标
- EXE_NAME_PREFIX: "QPyPack" # 用程序前缀
+ MAIN_SCRIPT: "qpypack/main.py" # Python 入口文件
+ ICON_PATH: "qpypack/icon.ico" # 默认应用程序图标 (Windows ICO)
+ EXE_NAME_PREFIX: "QPyPack" # 应用程序名前缀
# -------------------------------------------------------------
# 🚀 功能开关
@@ -21,11 +21,11 @@ env:
PUBLISH_TO_PYPI: "true" # 是否发布到 PyPI 官方源
# -------------------------------------------------------------
- # 软件元数据
+ # 软件元数据 (Windows 平台适用)
COMPANY_NAME: "QwejayHuang"
PRODUCT_NAME: "QPyPack"
LEGAL_COPYRIGHT: "Copyright (C) QwejayHuang"
- FILE_DESCRIPTION: ""
+ FILE_DESCRIPTION: "Automation Python Packaging and Freezing Suite"
# =================================================================
jobs:
@@ -116,12 +116,25 @@ jobs:
skip-existing: true
continue-on-error: true
- build-windows-exe:
- name: Build Windows EXE
- runs-on: windows-latest
+ build-executables:
+ name: Build Native Executables
+ runs-on: ${{ matrix.os }}
permissions:
contents: write
needs: publish-pypi
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - os: windows-latest
+ platform: windows
+ archive_ext: .zip
+ - os: macos-latest
+ platform: macos
+ archive_ext: .zip
+ - os: ubuntu-latest
+ platform: linux
+ archive_ext: .tar.gz
steps:
- name: Checkout source code
uses: actions/checkout@v4
@@ -138,7 +151,7 @@ jobs:
VERSION=${TAG_NAME#v}
echo "VERSION=$VERSION" >> $GITHUB_ENV
- - name: Sync version in Windows Workspace
+ - name: Sync version in Workspace
shell: python
run: |
import re
@@ -146,7 +159,7 @@ jobs:
version = os.environ['VERSION']
main_path = os.environ['MAIN_SCRIPT']
- print(f"Target version to sync on Windows: {version}")
+ print(f"Target version to sync on {os.name}: {version}")
if os.path.exists(main_path):
with open(main_path, 'r', encoding='utf-8') as f:
@@ -154,14 +167,15 @@ jobs:
new_content = re.sub(r'(__version__\s*=\s*["\'])[^"\']+(["\'])', f'\\g<1>{version}\\g<2>', content, count=1)
with open(main_path, 'w', encoding='utf-8') as f:
f.write(new_content)
- print(f"Windows workspace {main_path} synchronized.")
+ print(f"Workspace {main_path} synchronized.")
- name: Install dependencies
run: |
python -m pip install --upgrade pip
- python -m pip install PySide6 pyinstaller
+ python -m pip install PySide6 pyinstaller Pillow
- name: Generate Windows Version Info File
+ if: matrix.platform == 'windows'
shell: python
run: |
import os
@@ -217,34 +231,81 @@ jobs:
with open('file_version_info.txt', 'w', encoding='utf-8') as f:
f.write(info_content.strip())
- - name: Build EXE with PyInstaller
+ - name: Configure Icon Options
+ shell: bash
+ run: |
+ ICON_OPTION=""
+ if [ "${{ matrix.platform }}" = "windows" ]; then
+ if [ -f "${{ env.ICON_PATH }}" ]; then
+ ICON_OPTION="--icon=\"${{ env.ICON_PATH }}\" --add-data=\"${{ env.ICON_PATH }};.\""
+ fi
+ elif [ "${{ matrix.platform }}" = "macos" ]; then
+ # macOS 优先检查工程是否存在标准的 .icns/icon.png,否则采用默认图标降级匹配
+ if [ -f "qpypack/icon.icns" ]; then
+ ICON_OPTION="--icon=\"qpypack/icon.icns\" --add-data=\"qpypack/icon.icns:.\""
+ elif [ -f "qpypack/icon.png" ]; then
+ ICON_OPTION="--icon=\"qpypack/icon.png\" --add-data=\"qpypack/icon.png:.\""
+ elif [ -f "${{ env.ICON_PATH }}" ]; then
+ ICON_OPTION="--icon=\"${{ env.ICON_PATH }}\" --add-data=\"${{ env.ICON_PATH }}:.\""
+ fi
+ elif [ "${{ matrix.platform }}" = "linux" ]; then
+ if [ -f "qpypack/icon.png" ]; then
+ ICON_OPTION="--add-data=\"qpypack/icon.png:.\""
+ elif [ -f "${{ env.ICON_PATH }}" ]; then
+ ICON_OPTION="--add-data=\"${{ env.ICON_PATH }}:.\""
+ fi
+ fi
+ echo "ICON_OPTION=$ICON_OPTION" >> $GITHUB_ENV
+
+ - name: Build EXE / APP / Binary with PyInstaller
shell: bash
run: |
PYINSTALLER_CMD="pyinstaller --clean --onefile --noconsole"
- if [ -f "file_version_info.txt" ]; then
+ # Windows 元数据配置
+ if [ "${{ matrix.platform }}" = "windows" ] && [ -f "file_version_info.txt" ]; then
PYINSTALLER_CMD="$PYINSTALLER_CMD --version-file=file_version_info.txt"
fi
- if [ -f "${{ env.ICON_PATH }}" ]; then
- PYINSTALLER_CMD="$PYINSTALLER_CMD --icon=\"${{ env.ICON_PATH }}\" --add-data=\"${{ env.ICON_PATH }};.\""
- else
- echo "Warning: Icon path '${{ env.ICON_PATH }}' not found. Packaging without a custom icon."
+ # macOS Bundle 属性标识符配置
+ if [ "${{ matrix.platform }}" = "macos" ]; then
+ PYINSTALLER_CMD="$PYINSTALLER_CMD --osx-bundle-identifier=\"com.qwejayhuang.qpypack\""
fi
- PYINSTALLER_CMD="$PYINSTALLER_CMD --name=\"${{ env.EXE_NAME_PREFIX }}_${{ env.VERSION }}\" ${{ env.MAIN_SCRIPT }}"
+ # 挂载图标参数
+ if [ -n "$ICON_OPTION" ]; then
+ PYINSTALLER_CMD="$PYINSTALLER_CMD $ICON_OPTION"
+ fi
- echo "Executing: $PYINSTALLER_CMD"
+ TARGET_NAME="${{ env.EXE_NAME_PREFIX }}_${{ env.VERSION }}"
+ PYINSTALLER_CMD="$PYINSTALLER_CMD --name=\"$TARGET_NAME\" ${{ env.MAIN_SCRIPT }}"
+
+ echo "Executing build: $PYINSTALLER_CMD"
eval $PYINSTALLER_CMD
- - name: Compress EXE to ZIP
+ - name: Compress Build Artifact (Windows)
+ if: matrix.platform == 'windows'
shell: pwsh
run: |
Compress-Archive -Path "dist/${{ env.EXE_NAME_PREFIX }}_${{ env.VERSION }}.exe" -DestinationPath "dist/${{ env.EXE_NAME_PREFIX }}_${{ env.VERSION }}.zip"
- - name: Upload ZIP to GitHub Release
+ - name: Compress Build Artifact (macOS)
+ if: matrix.platform == 'macos'
+ shell: bash
+ run: |
+ cd dist
+ zip -r "${{ env.EXE_NAME_PREFIX }}_${{ env.VERSION }}.zip" "${{ env.EXE_NAME_PREFIX }}_${{ env.VERSION }}.app"
+
+ - name: Compress Build Artifact (Linux)
+ if: matrix.platform == 'linux'
+ shell: bash
+ run: |
+ cd dist
+ tar -czvf "${{ env.EXE_NAME_PREFIX }}_${{ env.VERSION }}.tar.gz" "${{ env.EXE_NAME_PREFIX }}_${{ env.VERSION }}"
+
+ - name: Upload Artifacts to GitHub Release
uses: softprops/action-gh-release@v2
with:
- files: dist/${{ env.EXE_NAME_PREFIX }}_${{ env.VERSION }}.zip
+ files: dist/${{ env.EXE_NAME_PREFIX }}_${{ env.VERSION }}${{ matrix.archive_ext }}
env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/README.md b/README.md
index ea11e0d..ed78aa4 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# QPyPack
-QPyPack 是一个为 Python 开发者设计的图形化打包配置工具。它将命令行打包流程转换为直观、简易的 GUI 界面操作,无需记忆繁琐的终端命令,即可辅助生成可执行文件。
+QPyPack 是一款基于 PyInstaller 与 Nuitka 的跨平台 Python 应用打包构建工具。它将繁琐的命令行打包流程转化为直观的可视化界面操作,无需记忆复杂的终端参数,即可高效生成原生可执行程序。
@@ -8,42 +8,48 @@ QPyPack 是一个为 Python 开发者设计的图形化打包配置工具。它
---
-## 核心设计:简化打包流程
+## 核心特性
-为了降低传统命令行打包的配置成本,解决依赖易冲突等问题,QPyPack 整合了以下辅助功能:
+为了降低传统命令行构建的配置成本,解决多平台下环境与依赖冲突问题,QPyPack 深度整合了以下工程化辅助功能:
-### 1. 简易的交互流程
-* **拖拽即装载**:无需手动输入路径,直接将您的 `.py` 或 `.pyw` 源代码文件拖入软件窗口,即可自动识别并准备构建。
-* **智能图标匹配**:脚本载入后,程序会自动检索同目录下的常用图标文件(如 `icon.ico` / `logo.ico` / `icon.svg` / `logo.svg`)并进行自动填充和预览。
-* **一键直达结果**:打包完成后,点击“打开输出目录”按钮即可自动打开资源管理器并定位到生成的程序。
+### 1. 直观的可视化交互
+* **拖放式载入(Drag & Drop)**:无需手动配置路径,直接将 `.py` 或 `.pyw` 源代码文件拖放至软件窗口,系统将自动载入并准备编译。
+* **图标自动检索与适配**:源程序导入后,系统会自动检索同级目录下的常用图像资产(如 `icon.ico` / `logo.ico` / `icon.svg` / `logo.svg`)进行格式适配、自动绑定与高清预览。
-### 2. 自动化的依赖与环境处理
-* **独立打包环境 (Venv)**:支持一键开启虚拟环境。工具会在临时目录下自动创建干净的沙盒环境,仅安装打包所需的库,有助于控制最终生成的可执行文件的体积。
-* **多维度依赖扫描**:
- * 自动检测并优先读取项目目录下的 `requirements.txt`。
- * 提供辅助的 AST(静态语法树)扫描选项,分析代码中导入的第三方模块,帮助查漏补缺隐式依赖。
-* **内置国内加速源**:预设清华大学等国内 PIP 镜像源,在沙盒中拉取打包工具或依赖库时提供加速通道。
+### 2. 依赖解析与虚拟环境隔离
+* **虚拟环境隔离(Virtualenv)**:支持一键在临时目录下创建独立的沙盒隔离环境,仅同步应用编译所需的依赖项,从而显著控制并精简最终可执行产物的体积。
+* **多维度依赖解析**:
+ * **清单同步**:自动检索并优先读取项目目录下的 `requirements.txt` 依赖声明。
+ * **AST 静态扫描**:提供辅助的 AST(静态语法树)深度分析,自动提取代码中导入的第三方依赖,精准补齐隐式导入模块(Hidden Imports)。
+* **内置 PyPI 加速源**:预设国内主流镜像加速源,大幅提升沙盒中拉取构建引擎与依赖库的速度。
-### 3. 可视化参数配置
-* **双引擎切换**:在下拉菜单中即可在 PyInstaller 和 Nuitka 之间灵活切换,相关的参数输入框与高级选项会根据所选引擎特性自动启用或隐藏。
-* **可视化版本属性**:无需编写复杂的版本配置文件,直接在界面文本框中输入版本号、公司名和产品描述,工具会自动将其写入生成的可执行文件元数据中。
-* **直观的资源添加**:通过交互式对话框选择需要打包的图片、数据文件夹,引擎会自动进行路径映射与资源封装。
+### 3. 高级编译参数控制
+* **双引擎自适应切换**:在图形界面上实现 PyInstaller 与 Nuitka 的灵活切换,参数面板与优化选项将根据所选引擎的特性进行自适应调整。
+* **应用元数据注入(Metadata)**:无需编写繁琐的规格文件,直接在界面输入版本号、公司名和产品描述,工具会自动将这些元数据注入二进制程序属性中(支持 Windows 资源区和 macOS Info.plist 写入)。
+* **附加资源可视化管理(Data Files)**:支持通过列表交互分别导入文件或文件夹,并支持通过双击列表直接修改打包后的相对释放路径(自适应释放路径映射)。
---
### 更新日志 (Changelog)
+**2.5.0**
+* **安全去耦与跨平台稳定性优化**:完全隔离 Windows 的 `winreg` 注册表静态调用,基于动态占位保障 macOS 与 Linux 等非 Windows 环境下的静态加载安全性。
+* **自适应图像格式转换引擎**:引入全新的 `convert_image_to_format` 弹性图形转译机制。可将 SVG、PNG、JPG 等格式自动适配转换为 Windows `.ico` 与 macOS `.icns`,内置“macOS sips -> Qt QImage -> Pillow”三重安全降级备份。
+* **应用元数据精准转译与写入**:在 Windows 系统下自动动态写入 `VSVersionInfo` 元数据,而在 macOS 平台下自动映射生成规范的 Bundle ID(如 `com.company.appname`),为后续的代码签名与公证提供标准数据格式。
+* **多维异构构建产物精准提炼**:重构底层产物移动(Move)与重置逻辑,精准捕获并转移不同编译模式下产生的文件实体或目录包(如 Windows 的 `.exe`,macOS 拥有独立目录结构的 `.app`,以及 UNIX 无后缀二进制),杜绝旧版本跨系统提取失败的问题。
+* **依赖声明与打包规范优化**:更新 `pyproject.toml`,补充标准 PyPI 分类器(Classifiers)及扩展项目链接(URLs),显式声明 `setuptools` 范围防止打包杂质,并将 Pillow 加入核心运行时依赖保障跨平台图标转译。
+
**2.4.6**
-* **全面跨平台适配 (Windows/macOS/Linux)**:修复了底层进程调用中 Windows 专有的 `creationflags` 导致其他系统崩溃的致命问题;重新构建 UNIX 环境(macOS/Linux)下的 Python 解释器探测与环境扫描器;修正 `version-file` 等平台特异性编译参数。
-* **重构现代化 UI 与高级 UX 设计**:摒弃了传统的“框列排版”,采用现代化的卡片式布局 (Card-based Layout)。为繁杂的“构建配置”引入了“胶囊式子导航 (Pill-Tabs)”,使其在功能极度丰富的情况下依然保持克制与优雅;全面处理因分辨率限制导致的内容挤压、截断问题。
-* **自定义 Python 解释器编译引擎**:新增了可全局跨版本指定底层解释器的核心功能。无论你的宿主是什么版本,只要在下拉列表选择(或手动浏览)目标 Python (例如极旧的 Python 3.5),工具即可在该对应的内核沙盒中完成纯正的代码编译与依赖对齐。
-* **全新极简风格的关于界面**:摒弃低质感排版,采用原生交互式动作行 (Action Row) 展示项目开源链接,点击可无缝跳转系统浏览器;解决了局部 ICO 图像渲染时的马赛克失真问题,保证界面绝对高清锐利。
+* **跨平台兼容性适配**:修复了底层进程调用中 Windows 平台专有的 `creationflags` 导致非 Windows 系统执行崩溃的致命异常;重构 UNIX 环境(macOS/Linux)下的 Python 解释器静态探测与虚拟沙盒环境分析器;过滤及剔除了平台特异性编译参数(如 `version-file` 等)。
+* **UI 现代化重构与 UX 体验升级**:废弃传统密集型排版,采用卡片式容器布局(Card-based Layout)。为复杂的构建参数面板引入“胶囊式子导航(Pill-Tabs)”,在功能丰富度与视觉克制力之间取得平衡;全面解决因低分辨率缩放引发的组件挤压、文本截断等视觉缺陷。
+* **多版本内核独立编译沙盒**:支持全局指定或浏览任意版本的 Python 解释器(例如兼容极旧的 Python 3.5 内核),程序将在对应的专属内核沙盒中完成代码解析、依赖对齐与独立编译分发,彻底与主控宿主环境解耦。
+* **关于界面视觉重构**:引入原生交互式动作行(Action Row)替代传统低质感排版,支持无缝调用系统浏览器跳转开源仓库;修复了部分 ICO 图标渲染时的抗锯齿失真问题,保证界面资产高清锐利。
**2.4.5**
-* **源码解析安全修复**:移除了在进行依赖扫描(AST 语法树解析)和控制台防闪退代码注入时,对源码换行符(CRLF 强转 LF)的过度干预操作,修复了因此导致的潜在多行字符串解析错误及文件破坏问题。
+* **源码解析安全修复**:移除了在进行依赖扫描(AST 语法树解析)和控制台防闪退代码注入时对源码换行符(CRLF 强转 LF)的干预操作,修复了由此导致的潜在多行字符串解析错误及文件破坏问题。
**2.4.4**
-* **附加资源可视化管理**:彻底废弃了易输错的单行文本框,全新引入列表化管理组件。支持通过图形化对话框分别添加“文件”和“文件夹”,并支持**双击列表条目直接修改打包后的相对释放路径**,操作更加直观极简。
-* **路径解析引擎升级**:配合新的可视化资源管理,重写了底层存储与引擎构建指令(`--add-data`)的拼接逻辑,彻底解决了旧版本中绝对路径(如包含盘符冒号)被错误切割的解析隐患。
+* **附加资源可视化管理**:废弃了易输错的单行文本框输入,引入列表化管理组件。支持通过图形化对话框分别添加“文件”和“文件夹”,并支持双击列表条目直接修改打包后的相对释放路径。
+* **路径解析引擎升级**:配合新的可视化资源管理,重写了底层存储与引擎构建指令(`--add-data`)的拼接逻辑,解决了旧版本中绝对路径被错误切割的解析隐患。
**2.4.3**
* **自定义依赖清单支持**:新增指定依赖文件路径配置。当多个 Python 脚本放置在同一文件夹管理时,可直接为各个脚本绑定不同的依赖清单(例如 `requirements1.txt`),无需再频繁手动重命名。
@@ -53,79 +59,35 @@ QPyPack 是一个为 Python 开发者设计的图形化打包配置工具。它
* **优化 SVG 图标转换容错**:针对 Windows 平台下 PyInstaller 对图标格式的限制,在宿主环境缺少特定图片格式插件时,新增优雅降级与日志提示机制,预防编译器因传入不支持的格式而引发崩溃。
**2.4.2**
-* **语法错误提示**:打包失败时,自动扫描完整构建日志,识别 Python 源码中的 IndentationError、SyntaxError 和 TabError。
-* **显示错误位置**:解析出出错文件名、行号及错误描述,帮助用户快速定位代码缺陷,程序区分“用户代码错误”与“打包工具故障”,避免混淆。
+* **编译语法异常诊断**:编译失败时,系统将自动扫描并重排完整的构建日志,精准识别源码中的 `IndentationError`、`SyntaxError` 和 `TabError` 语法错误。
+* **精准定位错误源**:解析并高亮展示发生语法错误的源文件名、行号及错误描述,区分“源码逻辑异常”与“打包工具故障”,避免混淆。
**2.4.1**
-* **重构设置TAB**:全面重构设置面板,将参数分类为“应用元数据”、“编译控制”、“资源与沙盒”、“软件设置”四大模块。
-* **保存路径配置**:支持将 EXE 输出至全局指定的自定输出目录,并支持对缺失的多级父目录进行全自动递归创建。
+* **模块化配置重构**:全面重构设置面板,将参数解耦并分类为“版本信息与元数据”、“编译控制”、“资源与沙盒”、“软件设置”四大模块。
+* **自定义归档目录**:支持将编译产物输出至全局指定的自定义输出目录,并支持对缺失的多级父目录进行全自动递归创建。
* **并发编译限制**:支持手动限制打包引擎并发编译时所能占用的最大 CPU 核心数,避免由于硬件满载引发设备死机。
-* **编译完成声音反馈**:引入非外部重度依赖的系统级音频通知,在打包成功或异常中断时给予不同的声音提示。
-* **自动持久化打包日志**:任务结束后支持在目标输出路径自动保存运行日志(.log 格式),极大提升异常分析排查的便利性。
+* **编译提示音效**:引入系统级轻量化音频通知,在打包成功或发生异常中断时给予不同的声音提示。
+* **编译日志自动保存**:任务结束后支持在目标输出路径自动保存运行日志(.log 格式),极大提升异常分析排查的便利性。
**2.4.0**
* **控制台防闪退机制**:在非 GUI 模式打包时,工具会自动在临时脚本尾部注入防闪退控制代码(针对 Windows 平台下双击运行的非交互式程序),提升用户体验。
* **提升文件删除的鲁棒性**:重构了临时目录清理机制,引入带重试延时与只读属性解除的 `robust_rmtree` 回收机制,减少因防病毒软件或进程占用导致清理失败的情况。
-* **新增云同步锁检测**:对于处于 OneDrive 等云同步盘锁定状态的文件,提供前置 I/O 异常检测,避免打包过程意外被系统挂起。
+* **云盘锁状态前置拦截**:针对处于云盘同步锁定状态的文件,提供前置 I/O 异常检测,避免打包过程意外被系统挂起。
* **完善引擎参数映射**:优化 Nuitka 的插件自动加载逻辑(自适应 PyQt/PySide 家族、numpy, matplotlib, tkinter 等常见重型库),使其配置更加贴合实际运行环境。
-* **新增 CI/CD 工作流**:推送版本标签自动触发 PyPI 发布,并同步编译 Windows 运行包(`.exe`)上传至 GitHub Release 附件。
-* **版本自动同步**:构建时自动对齐 `pyproject.toml` 与入口脚本中的版本号,降低手动维护出错率。
-
-**2.3.6**
-* **新增 SVG 格式图标支持**:支持加载 `icon.svg` / `logo.svg` 作为程序图标。
-* **增加 `.pyw` 格式脚本支持**:全面支持无控制台窗口的 Python 脚本格式,优化拖拽校验与文件选择过滤器。
-* **修复 Windows 绝对路径分割问题**:重构附加资源解析器,避免绝对路径中的盘符冒号(如 `C:`)被错误截断,确保跨引擎数据装载安全。
-* **优化 AST 本地依赖过滤**:安装前主动检索当前目录结构,自动过滤本地辅助模块或文件夹,防止向 PyPI 发起不必要的安装请求。
-* **强化元数据清理机制**:将临时版本配置文件的生命周期管理提升至最高级别,确保即使在未启用“一键清理”时,构建结束也会无条件收回临时文件。
-
-**2.3.5**
-* **重构依赖拉取架构**:采用三重安全检测(Requirements -> Pipreqs -> AST 源码扫描),即使前置分析工具漏报,最终的 AST 扫描也会尝试抓取核心隐式库。
-* **工作区优化**:将 `QPyPack.ini` 配置文件平滑迁移至全局系统路径(`~/.qpypack/config.ini`),并将所有 AST 缓存、依赖清单等中间产物全部转移至操作系统 `Temp` 目录,避免对用户的工程代码区造成额外文件占用。
-* **构建日志标准化**:重构控制台与 UI 日志的输出,引入规范化的生命周期状态标签(如 `[Init]`, `[Env]`, `[Deps]`, `[Build]`, `[Pack]`),提升状态诊断的清晰度。
-* **虚拟环境静默优化**:分配独立隔离的 venv 打包沙盒后,首个动作变更为自动升级 `pip`,减少因旧版本引起的警告提示。
-* **核心 BUG 修复**:修复了因底层 `configparser` 映射字典读取异常,导致抛出 `'dict' object has no attribute 'lower'` 的问题。
-
-**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**
-* 规范目录结构,优化内部沙盒与临时文件命名规范。
-* **超时守护 (Watchdog)**:为关键打包步骤设置超时限制,防止打包过程因网络等外界干扰无限制挂起。
-* **依赖推导优化**:pipreqs 增加 15 秒超时保护,失败时自动降级为本地纯静态 AST 解析。
-* **智能命名输出**:自动解析脚本版本,预设输出文件名为 `{Name}_{Version}` 格式。
-* **编译兼容扩展**:Nuitka 编译模式新增对 PyQt6、PySide、NumPy、Matplotlib 等常用插件的自适应识别。
-
-**2.1.0**
-* 将 Nuitka 单文件打包解压路径调整为 `%LOCALAPPDATA%`。
-* 增加构建失败的 UI 动画反馈。
-* 改善打包程序防杀毒软件误报拦截的兼容表现。
-
-**2.0.1**
-* 修正 Nuitka 参数传递方式:采用更符合 Nuitka 规范的单参数追加形式:`cmd.append(f"--include-data-files={src}={dest}")`。
-* 加入平台感知后缀:动态识别操作系统(Windows 使用 `.exe` 后缀,而 macOS/Linux 不使用后缀),避免跨平台定位产物失败。
-* 安全过滤平台专有参数:针对 Windows 特有的元数据写入,只有当 `os.name == "nt"` 时才追加对应指令。
+* **持续集成与自动发布**:新增 GitHub Actions 工作流,推送版本标签自动触发 PyPI 发布,并同步编译运行包上传至 GitHub Release。
---
## 快速上手
-只需三步,即可完成打包:
+只需三步,即可完成打包编译:
-1. **拖入脚本**:将您的 Python 脚本(`.py` 或 `.pyw`)拖拽至主界面。
-2. **选择引擎**:点击右下角设置,在“基础打包”中选择您想使用的引擎(默认推荐 PyInstaller 进行快速验证)。
-3. **开始构建**:返回主界面点击“开始构建”,并在下方展开的控制台中实时查看打包进度。
+1. **导入源文件**:将您的 Python 脚本(`.py` 或 `.pyw`)拖拽至主界面。
+2. **配置编译引擎**:点击右下角设置按钮,在“编译参数”中选择您想使用的编译引擎与编译模式。
+3. **执行编译任务**:返回主界面点击“开始构建”,即可在下方展开的控制台中实时查看打包进度。
---
-### 启动应用
+### 运行启动
在 Python >= 3.8 环境中执行以下命令安装并启动程序:
```bash
@@ -135,7 +97,7 @@ qpypack
### 预编译版本下载
若不想配置 Python 环境,可直接下载打包好的独立可执行文件:
-[GitHub Releases 下载链接](https://github.com/Qwejay/PyPack/releases)
+[下载链接](https://github.com/Qwejay/PyPack/releases)
## Star History
diff --git a/qpypack/main.py b/qpypack/main.py
index 5e98648..818caa7 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.0"
+__version__ = "2.5.1"
__author__ = "QwejayHuang"
__company__ = "QwejayHuang"
__description__ = "基于 PyInstaller 与 Nuitka 的跨平台 Python 应用打包构建工具"
@@ -161,17 +161,23 @@ def extract_imports_via_ast(script_path, python_exe):
def get_svg_icon(name, color="#5F6368", size=24):
path_data = MATERIAL_ICONS.get(name, "")
- svg_str = f''
+ svg_str = f''
renderer = QSvgRenderer()
renderer.load(svg_str.encode('utf-8'))
- pixmap = QPixmap(size, size)
+
+ 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)
@@ -299,8 +305,15 @@ def find_system_python():
candidates.append(exe)
except: pass
+ seen = set()
+ unique_candidates = []
for cand in candidates:
cand = os.path.normpath(cand)
+ if cand not in seen:
+ seen.add(cand)
+ unique_candidates.append(cand)
+
+ for cand in unique_candidates:
if not os.path.exists(cand): continue
if os.name == 'nt' and "WindowsApps" in cand:
@@ -991,9 +1004,9 @@ class SettingsPanel(QWidget):
self.tab_pref = QWidget()
self.tab_about = QWidget()
- self.tabs.addTab(self.tab_build, " 🎛️ 构建参数")
- self.tabs.addTab(self.tab_pref, " ⚙️ 全局设置")
- self.tabs.addTab(self.tab_about, " ℹ️ 关于")
+ self.tabs.addTab(self.tab_build, get_svg_icon('package', "#5F6368", 16), "构建参数")
+ self.tabs.addTab(self.tab_pref, get_svg_icon('settings', "#5F6368", 16), "全局设置")
+ self.tabs.addTab(self.tab_about, get_svg_icon('info', "#5F6368", 16), "关于")
self.build_build_tab()
self.build_pref_tab()
@@ -1445,9 +1458,9 @@ class SettingsPanel(QWidget):
btn.clicked.connect(lambda: __import__('webbrowser').open(url))
return btn
- btn_github = create_link_btn("🌍 GitHub 仓库", "https://github.com/qwejay/QPyPack")
- btn_issue = create_link_btn("🐞 反馈与建议", "https://github.com/qwejay/QPyPack/issues")
- btn_pypi = create_link_btn("📦 PyPI 主页", "https://pypi.org/project/qpypack/")
+ btn_github = create_link_btn("GitHub 仓库", "https://github.com/qwejay/QPyPack")
+ btn_issue = create_link_btn("反馈与建议", "https://github.com/qwejay/QPyPack/issues")
+ btn_pypi = create_link_btn("PyPI 主页", "https://pypi.org/project/qpypack/")
btn_lay.addWidget(btn_github)
btn_lay.addWidget(btn_issue)
@@ -1457,7 +1470,7 @@ class SettingsPanel(QWidget):
main_lay.addStretch(1)
- rights_lbl = QLabel(f"Copyright © {__company__}. All rights reserved.")
+ rights_lbl = QLabel(f"Copyright © {__company__}.")
rights_lbl.setStyleSheet("font-size: 12px; color: #bdc1c6; font-weight: bold;")
rights_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
main_lay.addWidget(rights_lbl)
@@ -1721,7 +1734,7 @@ class PackingThread(QThread):
self.progress.emit(f"[ERROR] 子进程调用失败:未检测到系统指令或外部程序 \"{cmd_name}\" ({e})")
return False
except Exception as e:
- self.progress.emit(f"[ERROR] 子进程执行发生系统级异常: {e}")
+ self.progress.emit(f"[ERROR] 子进程执行发生 system 级异常: {e}")
return False
finally:
if timer:
@@ -2080,7 +2093,7 @@ class PackingThread(QThread):
err_info = self.detect_python_syntax_errors()
if err_info["is_code_error"]:
msg = (
- f"[❌ 语法异常] 源程序存在语法不合规或缩进异常错误!\n"
+ f"[语法异常] 源程序存在语法不合规或缩进异常错误!\n"
f" - 错误源文件: {err_info['file']}\n"
f" - 异常类型: {err_info['type']}\n"
f" - 异常位置: 第 {err_info['line']} 行附近\n"
@@ -2136,7 +2149,7 @@ class MainWindow(QMainWindow):
self.init_ui()
def init_style(self):
- self.setWindowTitle(f"{__app_name__} {__version__} - {__author__}")
+ self.setWindowTitle(f"{__app_name__} {__version__}")
self.setMinimumSize(680, 620)
self.resize(680, 620)
@@ -2229,12 +2242,55 @@ class MainWindow(QMainWindow):
self.stacked_layout.addWidget(self.settings_panel)
self.stacked_layout.setCurrentWidget(self.main_panel)
- self.statusBar = self.statusBar()
+ self.status_bar = self.statusBar()
self.status_label = QLabel(" 状态: 准备就绪")
- self.statusBar.addWidget(self.status_label)
+ self.status_bar.addWidget(self.status_label)
+
+ self.copyright_label = QLabel(f"Copyright © {__company__}. ")
+ 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 set_status(self, text):
+ self.status_label.setText(text)
+ 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'):
+ return
+ text = self.status_label.text()
+ metrics = self.status_label.fontMetrics()
+ text_width = metrics.horizontalAdvance(text)
+
+ copyright_metrics = self.copyright_label.fontMetrics()
+ copyright_width = copyright_metrics.horizontalAdvance(self.copyright_label.text())
+
+ if text_width + copyright_width + 60 > self.status_bar.width():
+ self.copyright_label.hide()
+ else:
+ self.copyright_label.show()
+
+ def resizeEvent(self, event):
+ super().resizeEvent(event)
+ self.adjust_status_bar()
+
+ def closeEvent(self, event):
+ if self.thread and self.thread.isRunning():
+ self.thread.cancel()
+ self.thread.wait()
+
+ if self.analysis_thread and self.analysis_thread.isRunning():
+ self.analysis_thread.terminate()
+ self.analysis_thread.wait()
+
+ 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.terminate()
+ self.settings_panel.scanner_thread.wait()
+
+ super().closeEvent(event)
+
def update_ui_state(self, state):
self.current_state = state
@@ -2324,7 +2380,7 @@ class MainWindow(QMainWindow):
self.script_path = path
self.drop_area.set_loading(Path(path).name)
- self.status_label.setText(f" 状态: 正在解析源文件 {Path(path).name} ...")
+ self.set_status(f" 状态: 正在解析源文件 {Path(path).name} ...")
self.btn_main.setEnabled(False)
if self.analysis_thread and self.analysis_thread.isRunning():
@@ -2380,7 +2436,7 @@ class MainWindow(QMainWindow):
self.drop_area.set_success(Path(path).name, custom_icon_path=auto_icon)
status_suffix = ",已配置为控制台模式" if not has_gui else ",已配置为无控制台模式"
- self.status_label.setText(f" 状态: 已载入源文件 {Path(path).name}{status_suffix}")
+ self.set_status(f" 状态: 已载入源文件 {Path(path).name}{status_suffix}")
if not self.log_container.isVisible(): self.toggle_log()
self.log.clear()
@@ -2393,7 +2449,7 @@ class MainWindow(QMainWindow):
def cancel_pack(self):
if self.thread and self.thread.isRunning():
self.thread.cancel()
- self.status_label.setText(" 状态: 构建已终止")
+ self.set_status(" 状态: 构建已终止")
self.drop_area.stop_build_anim()
self.update_ui_state("ready")
@@ -2480,7 +2536,7 @@ class MainWindow(QMainWindow):
self.thread.finished.connect(self.on_pack_finished)
self.thread.start()
- self.status_label.setText(f" 状态: 正在通过 {engine} 构建...")
+ self.set_status(f" 状态: 正在通过 {engine} 构建...")
self.update_ui_state("building")
self.drop_area.start_build_anim()
@@ -2494,11 +2550,11 @@ class MainWindow(QMainWindow):
if success:
icon_path = self.settings_panel.icon_edit.text().strip()
self.drop_area.show_success(icon_path)
- self.status_label.setText(" 状态: 构建完成 ✅")
+ self.set_status(" 状态: 构建完成")
self.update_ui_state("done")
else:
self.drop_area.show_failure()
- self.status_label.setText(" 状态: 构建失败 ❌")
+ self.set_status(" 状态: 构建失败")
self.update_ui_state("failed")
def open_dist(self):
@@ -2525,7 +2581,7 @@ class MainWindow(QMainWindow):
self.log.clear()
if self.log_container.isVisible(): self.toggle_log()
self.drop_area.reset()
- self.status_label.setText(" 状态: 工作区已重置")
+ self.set_status(" 状态: 工作区已重置")
self.update_ui_state("idle")
def append_log(self, msg):