diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cd9d821..b9aa97c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,41 +5,31 @@ on: tags: - 'v*' -# ================================================================= -# ⚙️ 项目参数配置区 -# ================================================================= env: - PROJECT_NAME: "qpypack" # 项目包名 - MAIN_SCRIPT: "qpypack/main.py" # Python 入口文件 - ICON_PATH: "qpypack/icon.ico" # 默认应用程序图标 (Windows ICO) - EXE_NAME_PREFIX: "QPyPack" # 应用程序名前缀 - - # ------------------------------------------------------------- - # 🚀 功能开关 - # ------------------------------------------------------------- - BUILD_PIP_PACKAGE: "true" # 是否构建 pip 安装包 - PUBLISH_TO_PYPI: "true" # 是否发布到 PyPI 官方源 - # ------------------------------------------------------------- - - # 软件元数据 (Windows 平台适用) - COMPANY_NAME: "QwejayHuang" - PRODUCT_NAME: "QPyPack" - LEGAL_COPYRIGHT: "Copyright (C) QwejayHuang" - FILE_DESCRIPTION: "Automation Python Packaging and Freezing Suite" -# ================================================================= + PROJECT_NAME: qpypack + MAIN_SCRIPT: qpypack/main.py + ICON_PATH: qpypack/icon.ico + EXE_NAME_PREFIX: QPyPack + BUILD_PIP_PACKAGE: true + PUBLISH_TO_PYPI: true + COMPANY_NAME: QwejayHuang + PRODUCT_NAME: QPyPack + LEGAL_COPYRIGHT: Copyright (C) QwejayHuang + FILE_DESCRIPTION: Automation Python Packaging and Freezing Suite jobs: publish-pypi: name: Publish to PyPI and Create Release runs-on: ubuntu-latest permissions: - contents: write - id-token: write + contents: write + id-token: write steps: - name: Checkout source code uses: actions/checkout@v4 with: - fetch-depth: 0 + ref: main + fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v5 @@ -57,34 +47,32 @@ jobs: print(f"Target version to sync: {version}") toml_path = 'pyproject.toml' - if os.path.exists(toml_path): - with open(toml_path, 'r', encoding='utf-8') as f: - content = f.read() - new_content = re.sub(r'(version\s*=\s*["\'])[^"\']+(["\'])', f'\\g<1>{version}\\g<2>', content, count=1) - with open(toml_path, 'w', encoding='utf-8') as f: - f.write(new_content) - print("Successfully synchronized pyproject.toml.") + if not os.path.exists(toml_path): + raise FileNotFoundError(f"Missing critical configuration: {toml_path}") + + with open(toml_path, 'r', encoding='utf-8') as f: + content = f.read() + new_content = re.sub(r'(version\s*=\s*["\'])[^"\']+(["\'])', f'\\g<1>{version}\\g<2>', content, count=1) + with open(toml_path, 'w', encoding='utf-8') as f: + f.write(new_content) + print("Successfully synchronized pyproject.toml.") main_path = os.environ['MAIN_SCRIPT'] - if os.path.exists(main_path): - with open(main_path, 'r', encoding='utf-8') as f: - content = f.read() - 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"Successfully synchronized {main_path}.") + if not os.path.exists(main_path): + raise FileNotFoundError(f"Missing critical entrypoint: {main_path}") + + with open(main_path, 'r', encoding='utf-8') as f: + content = f.read() + 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"Successfully synchronized {main_path}.") - name: Commit and push version updates back to repository run: | git config --global user.name "github-actions[bot]" git config --global user.email "github-actions[bot]@users.noreply.github.com" - - if [ -f "pyproject.toml" ]; then - git add pyproject.toml "${{ env.MAIN_SCRIPT }}" - else - git add "${{ env.MAIN_SCRIPT }}" - fi - + git add pyproject.toml "${{ env.MAIN_SCRIPT }}" git commit -m "chore: bump version to ${{ github.ref_name }} [skip ci]" || echo "No version changes to commit" git push origin HEAD:main @@ -99,50 +87,56 @@ jobs: run: python -m build - name: Create GitHub Release + if: env.BUILD_PIP_PACKAGE == 'true' uses: softprops/action-gh-release@v2 with: files: | dist/*.whl dist/*.tar.gz - fail_on_unmatched_files: false generate_release_notes: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Publish to PyPI - if: env.PUBLISH_TO_PYPI == 'true' && env.BUILD_PIP_PACKAGE == 'true' + if: env.BUILD_PIP_PACKAGE == 'true' && env.PUBLISH_TO_PYPI == 'true' uses: pypa/gh-action-pypi-publish@release/v1 with: skip-existing: true - continue-on-error: true - build-executables: - name: Build Native Executables + build-binaries: + name: Build Standalone for ${{ matrix.os }} (${{ matrix.arch }}) + needs: publish-pypi runs-on: ${{ matrix.os }} permissions: - contents: write - needs: publish-pypi + contents: write strategy: fail-fast: false matrix: include: - os: windows-latest - platform: windows - archive_ext: .zip - - os: macos-latest - platform: macos - archive_ext: .zip + arch: x64 + artifact_name: QPyPack.exe + package_extension: zip + - os: macos-14 + arch: arm64 + artifact_name: QPyPack.app + package_extension: zip - os: ubuntu-latest - platform: linux - archive_ext: .tar.gz + arch: x64 + artifact_name: QPyPack + package_extension: tar.gz + steps: - name: Checkout source code uses: actions/checkout@v4 + with: + ref: main - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.10' + cache: 'pip' - name: Extract version shell: bash @@ -151,31 +145,8 @@ jobs: VERSION=${TAG_NAME#v} echo "VERSION=$VERSION" >> $GITHUB_ENV - - name: Sync version in Workspace - shell: python - run: | - import re - import os - - version = os.environ['VERSION'] - main_path = os.environ['MAIN_SCRIPT'] - 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: - content = f.read() - 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"Workspace {main_path} synchronized.") - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install PySide6 pyinstaller Pillow - - name: Generate Windows Version Info File - if: matrix.platform == 'windows' + if: runner.os == 'Windows' shell: python run: | import os @@ -185,13 +156,6 @@ jobs: parts = re.findall(r'\d+', version) v_num = (parts + ['0', '0', '0', '0'])[:4] v_tuple = ",".join(v_num) - - company = os.environ.get('COMPANY_NAME', 'My Company') - product = os.environ.get('PRODUCT_NAME', 'My Product') - copyright_str = os.environ.get('LEGAL_COPYRIGHT', 'Copyright') - description = os.environ.get('FILE_DESCRIPTION', 'Python Executable') - exe_name = os.environ.get('EXE_NAME_PREFIX', 'App') - print(f"Windows FileVersion tuple parsed: ({v_tuple})") info_content = f""" @@ -212,13 +176,13 @@ jobs: StringTable( '040904B0', [ - StringStruct('CompanyName', '{company}'), - StringStruct('FileDescription', '{description}'), + StringStruct('CompanyName', '${{ env.COMPANY_NAME }}'), + StringStruct('FileDescription', '${{ env.FILE_DESCRIPTION }}'), StringStruct('FileVersion', '{version}'), - StringStruct('InternalName', '{exe_name}'), - StringStruct('LegalCopyright', '{copyright_str}'), - StringStruct('OriginalFilename', '{exe_name}.exe'), - StringStruct('ProductName', '{product}'), + StringStruct('InternalName', '${{ env.EXE_NAME_PREFIX }}'), + StringStruct('LegalCopyright', '${{ env.LEGAL_COPYRIGHT }}'), + StringStruct('OriginalFilename', '${{ env.EXE_NAME_PREFIX }}.exe'), + StringStruct('ProductName', '${{ env.PRODUCT_NAME }}'), StringStruct('ProductVersion', '{version}') ] ) @@ -231,81 +195,50 @@ jobs: with open('file_version_info.txt', 'w', encoding='utf-8') as f: f.write(info_content.strip()) - - name: Configure Icon Options + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install PySide6 Pillow pyinstaller + + - name: Build with PyInstaller (Windows with Version Info) + if: runner.os == 'Windows' + run: | + pyinstaller --clean --onefile --noconsole --version-file=file_version_info.txt --icon=${{ env.ICON_PATH }} --add-data "${{ env.ICON_PATH }};." --name="${{ env.EXE_NAME_PREFIX }}_${{ env.VERSION }}" ${{ env.MAIN_SCRIPT }} + + - name: Build with PyInstaller (macOS / Linux) + if: runner.os != 'Windows' 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 + if [ "${{ runner.os }}" = "macOS" ]; then + pyinstaller --onefile --noconsole --name=${{ env.EXE_NAME_PREFIX }} --icon=${{ env.ICON_PATH }} ${{ env.MAIN_SCRIPT }} + else + pyinstaller --onefile --noconsole --name=${{ env.EXE_NAME_PREFIX }} ${{ env.MAIN_SCRIPT }} fi - echo "ICON_OPTION=$ICON_OPTION" >> $GITHUB_ENV - - name: Build EXE / APP / Binary with PyInstaller - shell: bash - run: | - PYINSTALLER_CMD="pyinstaller --clean --onefile --noconsole" - - # Windows 元数据配置 - if [ "${{ matrix.platform }}" = "windows" ] && [ -f "file_version_info.txt" ]; then - PYINSTALLER_CMD="$PYINSTALLER_CMD --version-file=file_version_info.txt" - fi - - # macOS Bundle 属性标识符配置 - if [ "${{ matrix.platform }}" = "macos" ]; then - PYINSTALLER_CMD="$PYINSTALLER_CMD --osx-bundle-identifier=\"com.qwejayhuang.qpypack\"" - fi - - # 挂载图标参数 - if [ -n "$ICON_OPTION" ]; then - PYINSTALLER_CMD="$PYINSTALLER_CMD $ICON_OPTION" - fi - - 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 Build Artifact (Windows) - if: matrix.platform == 'windows' + - name: Package Artifact (Windows) + if: runner.os == 'Windows' shell: pwsh run: | - Compress-Archive -Path "dist/${{ env.EXE_NAME_PREFIX }}_${{ env.VERSION }}.exe" -DestinationPath "dist/${{ env.EXE_NAME_PREFIX }}_${{ env.VERSION }}.zip" + $OUT_NAME = "${{ env.EXE_NAME_PREFIX }}-${{ env.VERSION }}-windows-${{ matrix.arch }}" + Compress-Archive -Path "dist/${{ env.EXE_NAME_PREFIX }}_${{ env.VERSION }}.exe" -DestinationPath "$OUT_NAME.zip" -Force - - name: Compress Build Artifact (macOS) - if: matrix.platform == 'macos' + - name: Package Artifact (macOS / Linux) + if: runner.os != 'Windows' shell: bash run: | + VERSION=${{ env.VERSION }} + OS_NAME=$(echo "${{ runner.os }}" | tr '[:upper:]' '[:lower:]') + OUT_NAME="${{ env.EXE_NAME_PREFIX }}-${VERSION}-${OS_NAME}-${{ matrix.arch }}" cd dist - zip -r "${{ env.EXE_NAME_PREFIX }}_${{ env.VERSION }}.zip" "${{ env.EXE_NAME_PREFIX }}_${{ env.VERSION }}.app" + if [ "${{ matrix.package_extension }}" = "zip" ]; then + zip -r "../${OUT_NAME}.zip" "${{ matrix.artifact_name }}" + else + tar -czf "../${OUT_NAME}.tar.gz" "${{ matrix.artifact_name }}" + fi - - 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 + - name: Append Standalone Binary to GitHub Release uses: softprops/action-gh-release@v2 with: - files: dist/${{ env.EXE_NAME_PREFIX }}_${{ env.VERSION }}${{ matrix.archive_ext }} + files: ${{ env.EXE_NAME_PREFIX }}-*.* env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/README.md b/README.md index ed78aa4..c4a359f 100644 --- a/README.md +++ b/README.md @@ -1,105 +1,163 @@ # QPyPack -QPyPack 是一款基于 PyInstaller 与 Nuitka 的跨平台 Python 应用打包构建工具。它将繁琐的命令行打包流程转化为直观的可视化界面操作,无需记忆复杂的终端参数,即可高效生成原生可执行程序。 +

+ 基于 PyInstaller 与 Nuitka 的跨平台 Python 应用可视化打包构建工具 +

-image -image + + PyPI version + + + Python versions + + + License + + + GitHub stars + +

+ +QPyPack 是一款致力于简化 Python 应用打包流程的可视化工具。它深度整合了 PyInstaller 与 Nuitka 两大主流编译引擎,将繁琐的终端命令行参数转化为直观、便捷的图形界面交互,帮助开发者更高效、高成功率地生成跨平台原生可执行程序。 + +--- + +## 📷 界面预览 (Screenshots) + +

+ QPyPack Main Window + QPyPack Settings Window

--- -## 核心特性 + +## 🚀 核心特性 (Key Features) 为了降低传统命令行构建的配置成本,解决多平台下环境与依赖冲突问题,QPyPack 深度整合了以下工程化辅助功能: -### 1. 直观的可视化交互 -* **拖放式载入(Drag & Drop)**:无需手动配置路径,直接将 `.py` 或 `.pyw` 源代码文件拖放至软件窗口,系统将自动载入并准备编译。 -* **图标自动检索与适配**:源程序导入后,系统会自动检索同级目录下的常用图像资产(如 `icon.ico` / `logo.ico` / `icon.svg` / `logo.svg`)进行格式适配、自动绑定与高清预览。 +### 1. 直观的可视化交互体验 +* 📥 **拖放式载入 (Drag & Drop)**:无需手动拷贝路径,直接将 `.py` 或 `.pyw` 源代码文件拖放至软件窗口,系统将全自动解析并载入工作区。 +* 🎨 **图标智能检索与自适应**:源程序导入后,系统会自动检索同级目录下的常用图像资产(如 `icon.ico` / `logo.ico` / `icon.svg` / `logo.svg`)进行自适应格式转换、自动绑定与高清预览。 -### 2. 依赖解析与虚拟环境隔离 -* **虚拟环境隔离(Virtualenv)**:支持一键在临时目录下创建独立的沙盒隔离环境,仅同步应用编译所需的依赖项,从而显著控制并精简最终可执行产物的体积。 -* **多维度依赖解析**: - * **清单同步**:自动检索并优先读取项目目录下的 `requirements.txt` 依赖声明。 - * **AST 静态扫描**:提供辅助的 AST(静态语法树)深度分析,自动提取代码中导入的第三方依赖,精准补齐隐式导入模块(Hidden Imports)。 -* **内置 PyPI 加速源**:预设国内主流镜像加速源,大幅提升沙盒中拉取构建引擎与依赖库的速度。 +### 2. 依赖解析与虚拟环境沙盒 +* 🛡️ **虚拟环境隔离 (Virtualenv Sandbox)**:支持一键在系统临时目录下创建隔离的虚拟沙盒,仅安装应用运行所需的最小依赖项,从而有效精简最终可执行产物的体积。 +* 🔍 **多维度依赖解析**: + * **配置同步**:自动检索并优先读取项目目录下的 `requirements.txt` 依赖声明。 + * **AST 静态扫描**:提供辅助的 AST(静态语法树)深度分析,自动提取代码中导入的非标准库模块,精准补齐隐式导入依赖(Hidden Imports)。 +* ⚡ **内置 PyPI 加速源**:预设主流镜像加速通道,大幅提升沙盒中拉取构建引擎与依赖库的速度。 -### 3. 高级编译参数控制 -* **双引擎自适应切换**:在图形界面上实现 PyInstaller 与 Nuitka 的灵活切换,参数面板与优化选项将根据所选引擎的特性进行自适应调整。 -* **应用元数据注入(Metadata)**:无需编写繁琐的规格文件,直接在界面输入版本号、公司名和产品描述,工具会自动将这些元数据注入二进制程序属性中(支持 Windows 资源区和 macOS Info.plist 写入)。 -* **附加资源可视化管理(Data Files)**:支持通过列表交互分别导入文件或文件夹,并支持通过双击列表直接修改打包后的相对释放路径(自适应释放路径映射)。 +### 3. 高级编译参数精准控制 +* ⚙️ **双引擎自适应切换**:在图形界面上实现 PyInstaller 与 Nuitka 的无缝切换,参数面板与优化选项将根据所选引擎的特性进行自适应调整。 +* 📝 **应用元数据注入 (Metadata)**:无需编写繁琐的规格文件,直接在界面输入版本号、公司名和产品描述,工具会自动将元数据注入二进制程序属性中(支持 Windows PE 结构和 macOS Info.plist 写入)。 +* 📂 **附加资源可视化管理 (Data Files)**:支持通过列表交互分别导入文件或文件夹,并支持双击列表条目直接修改打包后的相对释放路径(自适应释放路径映射)。 --- -### 更新日志 (Changelog) -**2.5.0** +## ⚡ 快速上手 (Quick Start) + +您可以通过以下两种方式之使用 QPyPack: + +### 方法一:通过 pip 安装并运行(推荐开发者使用) + +在 Python >= 3.8 的环境中,执行以下命令进行安装与启动: + +```bash +# 安装 QPyPack +pip install qpypack + +# 启动程序 +qpypack +``` + +### 方法二:下载免安装独立运行版(推荐普通用户使用) + +如果您不想配置本地 Python 环境,可直接在 Release 页面下载对应系统的预编译打包版本: +👉 [下载页面](https://github.com/Qwejay/QPyPack/releases) + +--- + +## 📅 更新日志 (Changelog) + +### [2.5.2] - 2026-07-15 +* **重构编译后端指令集**:将 PyInstaller 与 Nuitka 的底层打包参数生成逻辑重构为两套**完全独立、互不干扰的构建分支**。这在架构上彻底消除了历史版本中因共享部分参数生成逻辑而导致的参数污染隐患,确保各自在运行中均符合官方最新的标准规范。 +* **优化 Nuitka 附加资源兼容性**:针对 Nuitka 编译器在处理单个附加数据文件时,若目标相对路径为 `.` 会导致编译崩溃(*contains illegal suffix*)的问题进行了自适应修复(`os.path.normpath` 路径标准化)。 +* **修正 Nuitka 排除模块指令**:将 Nuitka 引擎下排除特定模块的参数由原先不规范的 `--exclude-module` 修正为 Nuitka 官方标准的 `--nofollow-import-to`,提升了打包时对冗余依赖库进行剪裁的安全性和有效性。 +* **完善 Windows 二进制属性映射**:修正并补充了 Nuitka 编译分支下 Windows PE 元数据的字段映射。将应用描述正确映射至 `--file-description`(此前误映射为产品名称)、应用名称映射至 `--product-name`,并同步补充了 `--product-version` 参数,使输出的可执行文件属性信息更趋完整。 + +### [2.5.1] - 2026-07-15 +* **多平台自动编译**:利用 GitHub Actions,工作流将会在 windows-latest、macos-latest 和 ubuntu-latest 三种原生系统容器中并发运行,并自动将各自生成的原生可执行产物打包归档,最后统一上传至该 Tag 对应的 GitHub Release 附件中。 +* **扁平风格**:清除 Emoji 表情,全面换装扁平化 Google 风格矢量图标。 +* **修改版权信息显示**:删除标题栏作者信息,增加状态栏右侧常驻版权声明(自适应隐藏)。 + +
+展开查看历史版本更新记录 (2.5.0 及以下) + +### [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 平台专有的 `creationflags` 导致非 Windows 系统执行崩溃的致命异常;重构 UNIX 环境(macOS/Linux)下的 Python 解释器静态探测与虚拟沙盒环境分析器;过滤及剔除了平台特异性编译参数(如 `version-file` 等)。 +### [2.4.6] +* **跨平台兼容性适配**:修复了底层进程调用中 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** +### [2.4.5] * **源码解析安全修复**:移除了在进行依赖扫描(AST 语法树解析)和控制台防闪退代码注入时对源码换行符(CRLF 强转 LF)的干预操作,修复了由此导致的潜在多行字符串解析错误及文件破坏问题。 -**2.4.4** +### [2.4.4] * **附加资源可视化管理**:废弃了易输错的单行文本框输入,引入列表化管理组件。支持通过图形化对话框分别添加“文件”和“文件夹”,并支持双击列表条目直接修改打包后的相对释放路径。 * **路径解析引擎升级**:配合新的可视化资源管理,重写了底层存储与引擎构建指令(`--add-data`)的拼接逻辑,解决了旧版本中绝对路径被错误切割的解析隐患。 -**2.4.3** +### [2.4.3] * **自定义依赖清单支持**:新增指定依赖文件路径配置。当多个 Python 脚本放置在同一文件夹管理时,可直接为各个脚本绑定不同的依赖清单(例如 `requirements1.txt`),无需再频繁手动重命名。 * **修复控制台模式下导入及资源读取异常**:调整了控制台防闪退代码注入逻辑,将生成的临时入口文件限制在源脚本同级目录下,规避了原版本在系统临时文件夹编译导致本地相对导入(Import)及相对路径资源失效的错误。 * **重构后台分析线程控制逻辑**:废弃了不安全的后台线程强制终止方式(`QThread.terminate()`),采用解绑信号的处理机制,降低了在快速拖拽或频繁切换脚本时程序偶发的界面卡死、闪退或死锁概率。 -* **提升 Nuitka Standalone 打包兼容性**:重构了 Nuitka 独立目录打包结束后的提取逻辑,改用动态扫描匹配 `.dist` 产物文件夹,避免因打包入口临时重命名而引发“未定位到有效产物”的构建异常。 +* **提升 Nuitka Standalone 打包兼容性**:重构了 Nuitka 自动匹配逻辑,改用动态扫描匹配 `.dist` 产物文件夹,避免因打包入口临时重命名而引发“未定位到有效产物”的构建异常。 * **优化 SVG 图标转换容错**:针对 Windows 平台下 PyInstaller 对图标格式的限制,在宿主环境缺少特定图片格式插件时,新增优雅降级与日志提示机制,预防编译器因传入不支持的格式而引发崩溃。 -**2.4.2** +### [2.4.2] * **编译语法异常诊断**:编译失败时,系统将自动扫描并重排完整的构建日志,精准识别源码中的 `IndentationError`、`SyntaxError` 和 `TabError` 语法错误。 * **精准定位错误源**:解析并高亮展示发生语法错误的源文件名、行号及错误描述,区分“源码逻辑异常”与“打包工具故障”,避免混淆。 -**2.4.1** +### [2.4.1] * **模块化配置重构**:全面重构设置面板,将参数解耦并分类为“版本信息与元数据”、“编译控制”、“资源与沙盒”、“软件设置”四大模块。 * **自定义归档目录**:支持将编译产物输出至全局指定的自定义输出目录,并支持对缺失的多级父目录进行全自动递归创建。 * **并发编译限制**:支持手动限制打包引擎并发编译时所能占用的最大 CPU 核心数,避免由于硬件满载引发设备死机。 * **编译提示音效**:引入系统级轻量化音频通知,在打包成功或发生异常中断时给予不同的声音提示。 * **编译日志自动保存**:任务结束后支持在目标输出路径自动保存运行日志(.log 格式),极大提升异常分析排查的便利性。 -**2.4.0** +### [2.4.0] * **控制台防闪退机制**:在非 GUI 模式打包时,工具会自动在临时脚本尾部注入防闪退控制代码(针对 Windows 平台下双击运行的非交互式程序),提升用户体验。 * **提升文件删除的鲁棒性**:重构了临时目录清理机制,引入带重试延时与只读属性解除的 `robust_rmtree` 回收机制,减少因防病毒软件或进程占用导致清理失败的情况。 * **云盘锁状态前置拦截**:针对处于云盘同步锁定状态的文件,提供前置 I/O 异常检测,避免打包过程意外被系统挂起。 * **完善引擎参数映射**:优化 Nuitka 的插件自动加载逻辑(自适应 PyQt/PySide 家族、numpy, matplotlib, tkinter 等常见重型库),使其配置更加贴合实际运行环境。 * **持续集成与自动发布**:新增 GitHub Actions 工作流,推送版本标签自动触发 PyPI 发布,并同步编译运行包上传至 GitHub Release。 ---- - -## 快速上手 - -只需三步,即可完成打包编译: - -1. **导入源文件**:将您的 Python 脚本(`.py` 或 `.pyw`)拖拽至主界面。 -2. **配置编译引擎**:点击右下角设置按钮,在“编译参数”中选择您想使用的编译引擎与编译模式。 -3. **执行编译任务**:返回主界面点击“开始构建”,即可在下方展开的控制台中实时查看打包进度。 +
--- -### 运行启动 -在 Python >= 3.8 环境中执行以下命令安装并启动程序: -```bash -pip install qpypack -qpypack -``` +## 🤝 参与贡献 (Contributing) -### 预编译版本下载 -若不想配置 Python 环境,可直接下载打包好的独立可执行文件: -[下载链接](https://github.com/Qwejay/PyPack/releases) +我们非常欢迎您的参与!如果您在测试或使用过程中发现任何异常,或者有更好的优化建议,可以通过以下方式参与贡献: -## Star History +1. **提交反馈**:在项目主页提交 [GitHub Issues](https://github.com/Qwejay/QPyPack/issues),并附带异常日志。 +2. **提交代码**:欢迎提交 Pull Requests,请在提交前确保您的代码符合标准规范并完成了多平台本地测试。 + +--- + +## 📄 开源协议 (License) + +本项目基于 **GPL-3.0** 开源许可协议。在遵守该开源许可协议的前提下,您可以自由分发、修改和二次开发。 + +--- + +## 📈 Star History diff --git a/pyproject.toml b/pyproject.toml index a86941c..601b466 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "qpypack" -version = "2.5.0" +version = "2.5.2" 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 818caa7..c65c6e6 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.1" +__version__ = "2.5.2" __author__ = "QwejayHuang" __company__ = "QwejayHuang" __description__ = "基于 PyInstaller 与 Nuitka 的跨平台 Python 应用打包构建工具" @@ -177,7 +177,6 @@ def get_svg_icon(name, color="#5F6368", size=24): return QIcon(pixmap) - def get_svg_pixmap(name, color="#5F6368", size=64): return get_svg_icon(name, color, size).pixmap(size, size) @@ -394,12 +393,6 @@ def play_alert(success=True): pass def convert_image_to_format(src_path, dest_path, dest_format): - """ - 自适应跨平台图像格式转换服务: - 1. macOS 系统环境优先调用内建高性能 sips 命令; - 2. 基于 Qt 框架 QImage API 进行图像二次转换渲染; - 3. 异常路径下采用 Pillow 图像处理库提供向后兼容转换。 - """ src = Path(src_path).resolve() dst = Path(dest_path).resolve() fmt = dest_format.lower() @@ -446,7 +439,6 @@ def convert_image_to_format(src_path, dest_path, dest_format): return False - class AnimatedButton(QPushButton): def __init__(self, text, parent=None): super().__init__(text, parent) @@ -487,7 +479,6 @@ class AnimatedButton(QPushButton): self.animation_group.start() super().leaveEvent(event) - class TargetIconWidget(QWidget): def __init__(self, parent=None): super().__init__(parent) @@ -669,7 +660,6 @@ class TargetIconWidget(QWidget): painter.drawPixmap(pix_rect, scaled_pix) painter.end() - class DropArea(QFrame): fileDropped = Signal(str) @@ -819,7 +809,6 @@ class DropArea(QFrame): self.label.setStyleSheet("QLabel { background: transparent; color: #5F6368; font-size: 16px; font-weight: bold; border: none; }") self.sub_label.setText("自动解析模块依赖、资源文件与隐式导入") - class PythonScannerThread(QThread): scan_done = Signal(dict) @@ -926,7 +915,6 @@ class PythonScannerThread(QThread): except: pass self.scan_done.emit(valid_pythons) - class SettingsPanel(QWidget): def __init__(self, parent=None): super().__init__(parent) @@ -1578,7 +1566,6 @@ class SettingsPanel(QWidget): def clear_resource(self): self.add_data_list.clear() - class ScriptAnalysisThread(QThread): analysis_done = Signal(str, str, str, str, set) @@ -1627,7 +1614,6 @@ class ScriptAnalysisThread(QThread): self.analysis_done.emit(app_name, version, author, desc, script_imports) - class PackingThread(QThread): progress = Signal(str) finished = Signal(bool, str) @@ -1934,12 +1920,23 @@ class PackingThread(QThread): if engine == "PyInstaller": self.temp_workpath = Path(tempfile.mkdtemp(prefix="qpypack_build_")).resolve() - cmd = [python_exe, "-m", "PyInstaller", "--clean", "--noconfirm", 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['onefile']: cmd.append("--onefile") - else: cmd.append("--onedir") + if self.params['onefile']: + cmd.append("--onefile") + else: + cmd.append("--onedir") - if self.params['noconsole']: cmd.append("--noconsole") + if self.params['noconsole']: + cmd.append("--noconsole") + else: + cmd.append("--console") if icon_path: cmd.extend(["--icon", icon_path]) @@ -1968,22 +1965,31 @@ class PackingThread(QThread): for excl in self.params.get('exclude_modules', '').split(','): if excl.strip(): cmd.extend(["--exclude-module", excl.strip()]) - + elif engine == "Nuitka": self.temp_out_dir = Path(tempfile.mkdtemp(prefix="nuitka_out_")).resolve() - cmd = [python_exe, "-m", "nuitka", "--remove-output", "--assume-yes-for-downloads", - f"--output-dir={self.temp_out_dir.as_posix()}", f"--output-filename={app_name}{ext}"] + cmd = [ + python_exe, "-m", "nuitka", + "--remove-output", + "--assume-yes-for-downloads", + f"--output-dir={self.temp_out_dir.as_posix()}", + f"--output-filename={app_name}{ext}" + ] cores = self.params.get('cpu_cores', os.cpu_count() or 2) cmd.append(f"--jobs={cores}") - if self.params['onefile']: cmd.append("--onefile") - else: cmd.append("--standalone") + if self.params['onefile']: + cmd.append("--onefile") + else: + cmd.append("--standalone") if self.params['noconsole']: cmd.append("--windows-console-mode=disable") if sys.platform == "darwin": cmd.append("--macos-create-app-bundle") + else: + cmd.append("--windows-console-mode=force") if icon_path: if os.name == "nt": @@ -1993,9 +1999,18 @@ class PackingThread(QThread): 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']}") + if self.params.get('ver_comp'): + cmd.append(f"--company-name={self.params['ver_comp']}") + if self.params.get('ver_desc'): + cmd.append(f"--file-description={self.params['ver_desc']}") + if self.params.get('app_name'): + cmd.append(f"--product-name={self.params['app_name']}") + if self.params.get('ver_ver'): + v_str = self.params['ver_ver'].strip() + v_nums = re.findall(r'\d+', v_str) + v_clean = ".".join((v_nums + ['0', '0', '0', '0'])[:4]) + cmd.append(f"--file-version={v_clean}") + cmd.append(f"--product-version={v_clean}") elif sys.platform == "darwin": if self.params.get('ver_comp'): cmd.append(f"--company-name={self.params['ver_comp']}") @@ -2004,8 +2019,6 @@ class PackingThread(QThread): comp = self.params.get('ver_comp', 'mycompany').strip().lower().replace(" ", "") bundle_id = f"com.{comp or 'anonymous'}.{app_name.lower().replace(' ', '')}" cmd.append(f"--macos-signed-app-name={bundle_id}") - - 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") @@ -2024,10 +2037,15 @@ class PackingThread(QThread): if r_type == 'dir': cmd.append(f"--include-data-dir={src_path}={dst}") else: - cmd.append(f"--include-data-files={src_path}={dst}") + filename = Path(src).name + if dst == ".": + nuitka_dst = filename + else: + nuitka_dst = os.path.normpath(os.path.join(dst, filename)).replace('\\', '/') + cmd.append(f"--include-data-files={src_path}={nuitka_dst}") for excl in self.params.get('exclude_modules', '').split(','): - if excl.strip(): cmd.append(f"--exclude-module={excl.strip()}") + if excl.strip(): cmd.append(f"--nofollow-import-to={excl.strip()}") cmd.append(script_posix) @@ -2137,7 +2155,6 @@ class PackingThread(QThread): robust_rmtree(cwd / p) Path(cwd / f"{app_name}.spec").unlink(missing_ok=True) - class MainWindow(QMainWindow): def __init__(self): super().__init__() @@ -2588,7 +2605,6 @@ class MainWindow(QMainWindow): self.log.append(msg) self.log.ensureCursorVisible() - def main(): app = QApplication(sys.argv) app.setStyle("Fusion")