ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python跨平台打包陷阱:setuptools与PyInstaller协作原理

Python跨平台打包陷阱:setuptools与PyInstaller协作原理 1. 为什么“打包成功”不等于“运行成功”一个被低估的跨平台交付鸿沟你有没有遇到过这样的场景在自己开发用的 macOS 上用pyinstaller main.py一键生成dist/main文件夹双击就能跑兴冲冲发给 Windows 同事对方点开直接弹窗报错——“找不到模块 xxx”、“No module named pkg_resources”、“ImportError: DLL load failed”再发给 Linux 服务器同事./main报错更干脆“Permission denied” 或 “cannot execute binary file: Exec format error”。这不是个别现象而是 Python 跨平台分发中最普遍、最隐蔽、也最容易被开发者轻视的交付断层。这个断层的本质不是 Python 本身不跨平台而是Python 的“源码可移植”和“二进制可执行文件可移植”是两套完全不同的逻辑体系。setuptools解决的是源码分发、依赖声明与安装时的环境适配问题它把你的代码变成一个.whl或.tar.gz包交给目标机器上的pip去解析setup.py、下载依赖、编译 C 扩展、写入site-packages而PyInstaller干的却是另一件事它在当前构建环境中把 Python 解释器、你的所有.py文件、所有import进来的第三方库包括它们的.so/.dll/.dylib动态链接库、甚至系统级的libc兼容层全部“快照”打包进一个独立的目录或单个文件里。它不关心目标机器上有没有pip也不管setup.py里写了什么install_requires——它只认你构建时那个环境里的“真实状态”。这就埋下了所有陷阱的种子。热搜词里反复出现的“setuptools 安装不上”、“pyinstaller 下载地址”、“python 安装详细步骤”表面看是新手入门问题实则暴露了更深层的认知偏差很多人以为只要pip install setuptools和pip install pyinstaller成功了打包就万事大吉。但真相是setuptools是你和 PyPI 生态的契约PyInstaller是你和操作系统内核的契约这两份契约的签署方、约束条件、失效场景全都不一样。我见过太多项目在 CI/CD 流水线上用 Ubuntu 镜像打包出一个main可执行文件部署到 CentOS 7 服务器上直接 core dump查了半天才发现是glibc版本太低而 PyInstaller 默认打包的glibc兼容策略是“取构建机版本”不是“取目标机最低版本”。所以这篇文章不讲“怎么用pyinstaller -F main.py”那太浅了。我要带你拆解的是当setuptools的setup.py和PyInstaller的spec文件同时存在一个项目里时它们如何协作、如何冲突、如何被误用为什么你在 macOS 上打包的.app在 Windows 上根本打不开为什么--onefile模式下os.getcwd()返回的路径会让人崩溃以及最关键的——如何设计一套真正可靠的、能覆盖 Windows/macOS/Linux 三端的自动化打包验证流程。这不是一个工具教程而是一份面向生产环境的 Python 交付工程实践手册。2. setuptools 的真实角色不是打包工具而是元数据与依赖契约的缔造者很多开发者把setup.py当成 PyInstaller 的前置步骤认为“先python setup.py sdist生成源码包再用 PyInstaller 打包”这是典型的本末倒置。setuptools的核心价值从来不是帮你生成可执行文件而是为整个 Python 生态定义一个标准化的元数据接口。它回答的问题是“这个项目叫什么作者是谁依赖哪些库入口点在哪里哪些文件属于这个包” 这些信息是pip install、conda install、IDE 的智能提示、甚至 PyInstaller 自动分析import语句时所依赖的底层依据。我们来看一个真实的setup.py片段它远比你想象的更“有心机”from setuptools import setup, find_packages setup( namemusic-manager, version2.0.0, descriptionA cross-platform music library organizer, authorYour Name, author_emailyouexample.com, packagesfind_packages(exclude[tests*, docs*]), install_requires[ PyQt55.15.0, mutagen1.45.0, requests2.25.0, # 注意这里一个看似无害的依赖 pywin32; platform_system Windows, pyobjc-framework-cocoa; platform_system Darwin, python-xlib; platform_system Linux, ], entry_points{ console_scripts: [ musicmgr music_manager.cli:main, ], gui_scripts: [ musicmgr-gui music_manager.gui:main, ] }, package_data{ music_manager: [resources/icons/*.png, resources/sounds/*.wav], }, python_requires3.8, )这段代码里藏着至少五个关键陷阱点而它们都会直接影响 PyInstaller 的最终产物2.1 条件依赖Conditional DependenciesPyInstaller 的“盲区”install_requires中的pywin32; platform_system Windows这种写法是 PEP 508 标准定义的环境标记Environment Markers。它的意思是“只有在 Windows 系统上安装时才需要pywin32这个包”。pip在安装时会严格遵守这个规则。但 PyInstaller 不会。当你在 macOS 上运行pyinstaller main.py时它会扫描你的main.py里所有import语句如果main.py里写了import win32apiPyInstaller 就会试图把pywin32打包进去——哪怕setup.py明确声明了它只在 Windows 上需要。结果就是你在 macOS 上打包出一个包含 Windows 专属 DLL 的可执行文件它在 macOS 上根本跑不起来因为win32api模块加载失败。我的实操经验是永远不要在主程序代码里做跨平台的import分支判断比如if sys.platform win32: import win32api。PyInstaller 的静态分析器无法理解这种运行时逻辑它会把所有import语句都当作“必须存在”。正确的做法是把平台专属的代码封装进独立的模块比如platform/win32_utils.py、platform/macos_utils.py然后在setup.py的packages列表里明确排除掉非当前平台的模块。2.2entry_pointsPyInstaller 的“自动入口发现”机制entry_points里的console_scripts和gui_scripts是setuptools提供给pip的“快捷方式生成器”。当你pip install -e .后pip会在bin/目录下创建一个名为musicmgr的 shell 脚本内容大致是#!/usr/bin/env python -m music_manager.cli。PyInstaller 在分析你的项目时如果检测到setup.py里定义了entry_points它会自动将这些入口点作为潜在的打包目标。这意味着你甚至可以不用写pyinstaller music_manager/cli.py而是直接pyinstaller --name musicmgr .注意最后的点PyInstaller 会去读取setup.py找到music_manager.cli:main并以此为起点进行依赖分析。但这带来了新的问题entry_points定义的是“模块路径函数名”而 PyInstaller 最终打包的是“可执行文件”。如果你的cli:main函数里又动态import了另一个模块而这个模块的路径没有被find_packages()捕获比如它放在src/目录下而find_packages()默认只找顶层目录PyInstaller 就会漏掉它。我踩过的最深的一个坑是一个cli.py里import config_loader而config_loader.py放在src/utils/下setup.py里packagesfind_packages()没有包含src导致打包后运行时报ModuleNotFoundError。解决方案很简单要么把src加入find_packages()的where参数要么在pyinstaller命令里显式加上--add-data src/utils/config_loader.py;utils。2.3package_data资源文件的“隐形搬运工”package_data告诉setuptools“这些非 Python 文件也是这个包的一部分请在安装时一并复制过去。” 对于 PyInstaller 来说这同样是一个重要的信号源。当你使用pyinstaller --collect-all music_manager时PyInstaller 会读取setup.py里的package_data并自动将resources/icons/*.png和resources/sounds/*.wav这些文件按照music_manager包的相对路径打包进dist/musicmgr目录里。但这里有个致命细节package_data的路径是相对于setup.py所在目录的而 PyInstaller 的--add-data参数其源路径是相对于当前工作目录的。如果你在项目根目录下运行pyinstaller一切正常但如果你在src/目录下运行pyinstaller ../main.py那么--add-data的源路径就变成了../resources/icons/而package_data依然指向resources/icons/两者就对不上了。提示永远在项目根目录即setup.py所在目录下执行所有打包命令。这是避免路径混乱的铁律。你可以写一个build.sh脚本第一行就是cd $(dirname $0)/..确保无论从哪里调用都先进入根目录。2.4python_requires构建环境的“隐形守门员”python_requires3.8这行看起来只是个声明但它对 PyInstaller 构建过程有决定性影响。PyInstaller 本身是一个 Python 应用它必须在一个满足python_requires的环境中运行。更重要的是PyInstaller 打包出来的可执行文件其内置的 Python 解释器版本严格等于你用来运行pyinstaller命令的那个 Python 解释器版本。也就是说如果你用 Python 3.9 构建那么dist/musicmgr里就嵌入了一个 Python 3.9 的解释器如果你用 Python 3.11 构建里面就是 Python 3.11。而python_requires声明的是你的源码兼容的最低 Python 版本它并不保证你的代码能在更高版本上完美运行——尤其是当你用了asyncio的新特性或者某个第三方库在 Python 3.11 里废弃了某个 API。我曾经维护一个音乐管理器它依赖PyQt5。PyQt5的5.15.0版本在 Python 3.11 上有一个已知的 bug会导致 GUI 窗口无法显示。而我们的setup.py里python_requires3.8CI 流水线默认用最新版 Python 构建结果每次发布都失败。最终解决方案是在pyproject.toml里用[build-system]显式指定构建用的 Python 版本并在 CI 脚本里强制pyenv local 3.10.12。python_requires不是摆设它是你控制构建环境的第一道防线。3. PyInstaller 的“黑箱”内部从源码到可执行文件的七步炼狱理解setuptools是为了厘清“契约”而理解PyInstaller的内部机制则是为了掌控“执行”。PyInstaller 不是一个简单的“压缩包生成器”它是一个复杂的、多阶段的、高度平台相关的构建流水线。它的官方文档只告诉你“pyinstaller -F main.py”却很少解释背后发生了什么。下面我将基于我在 macOS、Windows 和 Linux 上分别构建超过 200 个不同项目的实战经验为你逐层拆解这七步“炼狱”。3.1 第一步AST 静态分析Abstract Syntax Tree AnalysisPyInstaller 启动后做的第一件事不是复制文件而是用 Python 自带的ast模块对你的main.py或你指定的入口脚本进行语法树解析。它会遍历 AST找出所有import语句、from ... import ...语句以及__import__()调用。这一步决定了“初始依赖图”的范围。但 AST 分析有其天然局限它无法解析字符串拼接的import比如module_name os; __import__(module_name)也无法理解getattr(__import__(sys), platform)这样的动态导入。这就是为什么很多项目在--onefile模式下运行时报ModuleNotFoundError根源往往在于那些“动态加载”的模块没有被 AST 捕获。避坑心得对于必须动态导入的模块PyInstaller 提供了--hidden-import参数。但更好的做法是在你的代码里用# pyinstaller: hidden-importxxx这样的注释来显式声明。PyInstaller 会扫描源码中的这类注释并将其加入依赖图。这比在命令行里写一堆--hidden-import更清晰、更可维护。3.2 第二步依赖图展开Dependency Graph Expansion拿到初始依赖图后PyInstaller 会递归地对每个模块执行同样的 AST 分析直到所有import语句都被穷尽。这个过程会产生一个巨大的、包含数千个.py文件的依赖图。但请注意这个图里只包含.py文件不包含.so/.dll/.dylib动态库。PyInstaller 会根据模块名去site-packages目录下查找对应的.py文件然后继续分析。这一步的耗时直接决定了pyinstaller命令的总时间。这也是为什么一个依赖了numpy、pandas、matplotlib的项目打包要花几分钟——因为这三个库自身就包含了数百个子模块。3.3 第三步二进制依赖解析Binary Dependency Resolution这才是跨平台陷阱的真正爆发点。当 PyInstaller 发现某个模块比如PIL._imaging.cpython-39-darwin.so是一个编译后的.so文件时它不会止步于此。它会调用系统级的工具来解析这个二进制文件的“依赖清单”在 Linux 上调用ldd命令在 macOS 上调用otool -L命令在 Windows 上调用dumpbin /dependents命令。这个清单里除了你期望的libjpeg.dylib、libpng.dylib还可能包含rpath/libpython3.9.dylib、/usr/lib/libSystem.B.dylib这样的系统库。PyInstaller 的任务就是把这些“运行时依赖”也一并打包进去。但问题来了/usr/lib/libSystem.B.dylib是 macOS 系统库它不能、也不应该被打包进你的应用里。PyInstaller 的策略是对于系统库它会尝试“重写”其加载路径让二进制文件在运行时去寻找它内置的、同名的、但版本兼容的副本。这个重写过程就是--rpath和--runtime-hook的用武之地。3.4 第四步资源收集与路径重写Resource Collection Path RewritingPyInstaller 会收集所有.py文件、.so/.dll/.dylib文件、package_data里声明的资源文件以及它通过ldd/otool找到的所有依赖库。然后它会对所有这些二进制文件里的“硬编码路径”进行批量重写将rpath/libpython3.9.dylib改为loader_path/../Frameworks/libpython3.9.dylibmacOS将libjpeg.so.62的DT_RUNPATH改为$ORIGIN/../libLinux将MSVCP140.dll的搜索路径改为应用目录Windows。这个重写过程是 PyInstaller 能实现“单文件”或“单目录”分发的核心技术。但重写不是万能的。有些库尤其是用CMake编译的会把路径写死在代码里而不是用动态链接器的机制。这时PyInstaller 就无能为力了你需要手动用--add-binary把这些库加进去并在代码里用sys._MEIPASS来定位它们。3.5 第五步引导程序Bootloader注入PyInstaller 会生成一个极小的、平台原生的 C 程序叫做 Bootloader。这个程序是最终可执行文件的“第一行代码”。它的职责是解压如果是--onefile模式或定位如果是--onedir模式所有打包好的资源设置好PYTHONPATH、LD_LIBRARY_PATHLinux、DYLD_LIBRARY_PATHmacOS等环境变量加载内置的 Python 解释器执行你的main.py。Bootloader 是用 C 写的它不依赖 Python所以它才能成为真正的“启动器”。这也是为什么--onefile模式下第一次运行会慢——因为 Bootloader 必须先把所有资源解压到一个临时目录通常是/tmp/_MEIxxxxxx然后再启动 Python。而--onedir模式则没有这个解压步骤启动更快但分发的是一个文件夹。3.6 第六步Hook 机制Hook System对抗“不可分析”的终极武器PyInstaller 的 Hook 系统是它最强大也最晦涩的功能。一个 Hook 就是一个 Python 脚本它告诉 PyInstaller“当你要打包xxx这个库时请额外执行以下操作”。PyInstaller 自带了超过 300 个官方 Hook覆盖了绝大多数主流库。但当你用到一个冷门库或者一个自己写的、有特殊资源加载逻辑的库时官方 Hook 就失效了。举个真实例子music-manager用到了mutagen库来读取 MP3 标签。mutagen本身没有.so文件但它在运行时会动态加载ctypes绑定的libmp3lame。PyInstaller 的 AST 分析根本看不到这个libmp3lame因为它不是通过import加载的。结果就是打包后的程序在读取 MP3 时mutagen报错说找不到libmp3lame。解决方案是写一个自定义 Hook# hooks/hook-mutagen.py from PyInstaller.utils.hooks import collect_dynamic_libs # 告诉 PyInstaller请把 libmp3lame.so/dll/dylib 也打包进来 binaries collect_dynamic_libs(mutagen)然后在pyinstaller命令里加上--additional-hooks-dir ./hooks。Hook 机制是 PyInstaller 的灵魂它让你有能力“教”这个工具如何正确处理任何你遇到的库。3.7 第七步签名与公证macOS OnlyApp Store 的入场券在 macOS 上--onefile打包出的只是一个可执行文件而--onedir打包出的是一个.app目录。后者才是 macOS 的“正规军”。为了让.app能在 Catalina 及以后的系统上正常运行你必须对其进行Code Signing代码签名。否则用户双击时会看到“已损坏无法打开”的警告。签名不是可选的而是强制的。更进一步如果你想把应用上架 Mac App Store还需要Notarization公证这是一个向 Apple 提交二进制文件、由 Apple 后台扫描恶意代码并返回一个公证票证的过程。签名和公证的命令链非常长# 1. 用你的开发者证书签名 codesign --force --deep --sign Developer ID Application: Your Name dist/musicmgr.app # 2. 验证签名是否有效 spctl --assess --type execute dist/musicmgr.app # 3. 提交公证需要 Apple 开发者账号 xcrun notarytool submit dist/musicmgr.app --keychain-profile AC_PASSWORD --wait # 4. 将公证票证 stapled钉入到 app 中 xcrun stapler staple dist/musicmgr.app这整个流程PyInstaller 不提供任何自动化支持。你必须把它写进你的 CI/CD 脚本里。而且--deep参数是关键它确保签名会递归地应用到.app内部的所有二进制文件上包括 PyInstaller 打包进去的libpython3.9.dylib。漏掉这一步签名就无效。4. 跨平台陷阱全景图Windows、macOS、Linux 的“各自为政”前面两章讲的是原理现在我们进入实战。我把过去三年里为music-manager项目在三端打包时遇到的、最典型、最高频、也最让人抓狂的陷阱整理成一张全景图。这张图不是按“错误类型”分类而是按“操作系统”分类因为每个系统的陷阱根源都来自其独特的 ABIApplication Binary Interface、文件系统语义和安全模型。操作系统典型陷阱现象根本原因一行修复命令/配置Windows双击.exe闪退事件查看器显示0xc000007b错误32/64 位架构不匹配你的 Python 是 32 位但PyQt5的Qt5Core.dll是 64 位或反之pyinstaller --archx64 main.py(确保 Python、PyInstaller、所有依赖库都是同一架构)Windows打包后图标不显示或显示为默认齿轮图标PyInstaller 默认不打包.ico文件且 Windows 资源编辑器需要特定格式pyinstaller --iconresources/icon.ico --onefile main.py且确保icon.ico包含 16x16, 32x32, 48x48, 256x256 多尺寸macOS.app双击无反应控制台显示Library not loaded: rpath/libpython3.9.dylibPyInstaller 的rpath重写失败或libpython3.9.dylib被系统 SIPSystem Integrity Protection保护pyinstaller --rpathloader_path/../Frameworks --one-directory main.py并在post-build.sh里用install_name_tool修正rpathmacOS.app在 Catalina 系统上提示“已损坏无法打开”缺少有效的 Developer ID Code Signingcodesign --force --deep --sign Developer ID Application: Your Name dist/musicmgr.appLinux./musicmgr报错cannot execute binary file: Exec format error构建机是 x86_64但目标机是 ARM64如 Raspberry Pi使用--target-archarm64参数或在 ARM64 机器上构建PyInstaller 不支持交叉编译Linux./musicmgr启动后 GUI 界面空白控制台无报错PyQt5依赖的libxcb系列库缺失或XDG_RUNTIME_DIR环境变量未设置pyinstaller --add-binary /usr/lib/x86_64-linux-gnu/libxcb*.so.1;. --onefile main.py并在启动脚本里export XDG_RUNTIME_DIR/tmp/runtime-$USER这张表里的每一个条目都对应着一次长达数小时的排查。下面我将选取其中三个最具代表性的案例展开讲述完整的排查链路和解决方案。4.1 Windows 陷阱0xc000007b错误的“架构迷宫”这个错误代码0xc000007b是 Windows 系统里最著名的“架构不匹配”错误。它意味着你的可执行文件.exe是 32 位的但它试图加载一个 64 位的 DLL或者反过来。PyInstaller 本身没有架构概念它只是忠实地把你构建环境里的所有东西打包进去。所以问题的根源永远在你的构建环境。排查链路首先确认你的 Python 解释器架构在命令行里运行python -c import platform; print(platform.architecture())。输出应该是(64bit, WindowsPE)或(32bit, WindowsPE)。然后确认你安装的所有关键依赖的架构。最简单的方法是用pip show PyQt5查看其安装路径然后去那个路径下找到Qt5Core.dll右键 - 属性 - 详细信息 - “文件版本”里会有一行“处理器架构”它会明确写着AMD64或x86。如果python是64bit而Qt5Core.dll是x86那你就掉进了陷阱。这是因为pip install PyQt5默认会安装与你的pip匹配的版本而pip的架构又取决于你安装 Python 时选择的安装包32 位还是 64 位。终极解决方案彻底卸载所有 Python 和 pip从 python.org 下载明确标注为64-bit的安装包重新安装。安装 PyInstaller 后用pyinstaller --version确认它是在 64 位环境下运行。在打包前运行pyinstaller --debugall main.py它会生成一个详细的日志其中会列出所有被分析的.dll文件及其架构。检查日志里是否有x86的 DLL。注意不要试图用--archx64参数来“欺骗”PyInstaller。这个参数只影响它生成的 Bootloader 的架构不影响它打包进去的第三方库的架构。架构一致性必须从源头——Python 解释器——开始保证。4.2 macOS 陷阱“已损坏无法打开”的签名地狱macOS 的 Gatekeeper 安全机制要求所有从互联网下载的应用都必须经过 Apple 认证的开发者签名否则就会被拦截。这个机制对开发者来说既是保护也是枷锁。排查链路双击.app看到警告后不要点“取消”而是点“显示简介”在“通用”标签页里你会看到“已损坏”字样。打开终端运行codesign --display --verbose4 dist/musicmgr.app。如果输出里有code object is not signed at all说明根本没签名。如果有签名但输出里有CSSMERR_TP_NOT_TRUSTED说明签名证书不是 Apple 的 Developer ID而是你自己生成的“Mac Development”证书这个证书只能用于开发调试不能用于分发。如果签名是 Developer ID但依然报错运行spctl --assess --type execute dist/musicmgr.app。如果输出rejected说明签名无效或不完整。解决方案首先去 Apple Developer 网站申请一个Developer ID Application证书并把它安装到你的钥匙串里。然后确保你的.app是用--onedir模式打包的因为--onefile模式打包的是一个可执行文件不是.appbundle无法被正确签名。执行签名命令时--deep参数必不可少它会递归签名.app内部的所有二进制文件。签名完成后务必用spctl命令验证而不是仅仅依赖双击测试。4.3 Linux 陷阱GUI 空白屏的libxcb缺失Linux 的桌面环境碎片化是跨平台开发者的噩梦。PyQt5在 Linux 上依赖libxcb系列库来与 X11 服务器通信。这些库通常不在site-packages里而是系统级的/usr/lib/x86_64-linux-gnu/目录下。PyInstaller 的ldd分析有时会漏掉它们因为PyQt5的Qt5Core.so可能是用dlopen()动态加载libxcb的而不是在编译时就链接进去的。排查链路在打包好的dist/musicmgr目录下运行ldd musicmgr | grep xcb。如果没有任何输出说明libxcb没有被 PyInstaller 找到并打包。手动去/usr/lib/x86_64-linux-gnu/目录下找到libxcb.so.1、libxcb-xinerama.so.0、libxcb-xfixes.so.0等文件。运行ldd /usr/lib/x86_64-linux-gnu/libxcb.so.1查看它自己的依赖确保没有遗漏。解决方案最稳妥的方式是用--add-binary参数把所有libxcb*文件显式添加进去pyinstaller --add-binary /usr/lib/x86_64-linux-gnu/libxcb*.so.1;. \ --add-binary /usr/lib/x86_64-linux-gnu/libX11.so.6;. \ --onefile main.py更优雅的方式是写一个 Hook让 PyInstaller 自动识别PyQt5的libxcb依赖。PyInstaller 官方已经为此提供了hook-PyQt5.py但你需要确保它被正确加载。5. 一套可落地的跨平台打包验证流水线从本地到 CI/CD理论和陷阱讲完了现在是时候给出一套能直接“抄作业”的、生产级的打包验证方案。这套方案的核心思想是不要相信任何一次成功的本地打包要建立一个自动化的、覆盖所有目标平台的、端到端的验证闭环。我把它分为三个层次本地快速验证、CI/CD 自动化构建、以及发布前的手动 Smoke Test。5.1 本地快速验证五分钟搞定三端基础可用性在你每次提交代码前都应该运行这个本地验证脚本。它不追求 100% 功能完整只验证最核心的“能启动、不崩溃、GUI 能显示”。#!/bin/bash # build-and-test.sh # Step 1: 清理旧构建 rm -rf dist build *.spec # Step 2: 为当前平台构建 echo Building for $(uname -s)... pyinstaller --onefile --name musicmgr --iconresources/icon.ico main.py # Step 3: 快速验证 echo Running smoke test... if [[ $OSTYPE darwin* ]]; then # macOS: 检查 .app 是否存在且能启动 if [ -d dist/musicmgr.app ]; then echo ✅ macOS .app exists # 尝试启动但不阻塞超时 5 秒 timeout 5 open -a dist/musicmgr.app echo ✅ macOS app launched || echo ⚠️ macOS app launch timeout else echo ❌ macOS .app missing fi elif [[ $OSTYPE linux-gnu* ]]; then # Linux: 检查可执行文件是否存在且能打印版本 if [ -f dist/musicmgr ]; then echo ✅ Linux binary exists chmod x dist/musicmgr timeout 5 ./dist/musicmgr --version echo ✅ Linux binary runs || echo ⚠️ Linux binary run timeout else echo ❌ Linux binary missing fi elif [[ $OSTYPE msys* ]] || [[ $OSTYPE cygwin* ]]; then # Windows (Git Bash): 检查 .exe 是否存在 if [ -f dist/musicmgr.exe ]; then echo ✅ Windows .exe exists # 在 Windows 上我们用 PowerShell 来启动并检查进程 echo Run powershell -Command \Start-Process ./dist/musicmgr.exe -WindowStyle Hidden\ to test else echo ❌ Windows .exe missing fi fi把这个脚本保存为build-and-test.sh每次改完代码运行./build-and-test.sh就能在 5 分钟内知道你的改动是否破坏了最基本的打包能力。5.2 CI/CD 自动化构建GitHub Actions 的三端矩阵本地验证只是第一步。真正的保障来自于在云端、用干净的、与生产环境一致的虚拟机上进行自动化构建和测试。以下是我在music-manager项目中使用的 GitHub Actions 工作流它实现了真正的三端并行构建# .github/workflows/build.yml name: Build Test Cross-Platform Binaries on: push: tags: - v*.*.* jobs: build-windows: runs-on: windows-latest steps: - uses: actions/checkoutv3 - name: Set up Python 3.10 uses: actions/setup-pythonv4 with: python-version: 3.10 - name: Install dependencies run: | pip install setuptools wheel pyinstaller PyQt5 mutagen - name: Build Windows executable run: pyinstaller --onefile --name musicmgr --icon
返回列表