把 Python 应用打包成 Mac 应用程序 — 完整指南
一、打包 macOS 应用的挑战与注意事项
在 macOS 上打包 Python 应用,和在 Windows 上打包有不少相似点(都要把 Python 运行时 + 依赖打包进来),但也有自己独特的挑战:
- macOS 的应用分发机制有沙箱、签名 (code signing)、Notarization(公证)等要求,新版 macOS 对未签名或未公证的应用会发出警告或拒绝运行。
- macOS 应用通常以
.app
包(bundle)的形式存在,里面结构比较规范(Contents、MacOS、Resources、Info.plist 等),需要构造正确的 bundle 结构和元数据。 - Python 扩展模块(
.so
、.dylib
)、动态库、插件等的依赖路径可能很复杂,可能需要处理加载路径、符号链接、签名一致性等问题。 - 在 Apple Silicon(ARM 架构)和 Intel 架构之间可能涉及构建架构兼容性问题(如果你要同时支持两种架构)。
- 要让普通用户双击就运行,还可能希望把
.app
放入.dmg
或.pkg
分发形式,并做好图标、安装体验、卸载等细节。
因此,在工具和流程选择时,需要兼顾“可靠性”“分发体验”“签名/公证支持”等多个维度。
下面我先介绍几种主流工具/方案,然后给出一个典型流程与调试建议。
二、常用工具 / 方案对比
以下是打包 Python 应用为 macOS 应用常见的几种方式:
工具 / 方案 | 适用场景 | 优点 | 缺点 / 限制 |
---|---|---|---|
py2app | 传统 macOS 平台打包工具(类似于 Windows 的 py2exe) | 专门为 macOS 设计,集成了 bundle 结构处理、Info.plist 填充、资源复制等逻辑。对于常见依赖(Tkinter、PyQt、Cocoa via PyObjC)支持较好。(py2app.readthedocs.io) | 构建有时不够灵活,对非常复杂依赖或大型科学计算库(numpy、scipy 等)可能需要手工调整;不支持在非 macOS 平台打包(你必须在 macOS 上运行打包工具)(PyPI) |
PyInstaller(macOS 模式 / 生成 bundle) | 如果你已经熟悉 PyInstaller,想用它在 macOS 上生成 .app bundle | 支持 “bundle (BUNDLE)” 模式,可以把 exe + 资源打包为 .app 包。你可以通过 spec 文件定制 Info.plist、bundle_identifier 等。(pyinstaller.org) | 对某些动态库、插件可能需要调整;打包后还要做代码签名、公证、优化资源,有时会遇到启动时权限 / 加密 /沙箱限制问题。(Haim Gelfenbeyn’s Blog) |
Platypus | 较小或脚本型的应用(如命令行脚本 / Python 脚本包装成 GUI 应用) | 用于把脚本包装为 macOS 应用包,较简单上手,适合小工具类型应用。(Sveinbjörn Þórðarson) | 不擅长很复杂的 GUI 或重度依赖的库;主要用于把脚本封装为应用启动器。 |
嵌入 Python 解释器 / Framework + Xcode 工程 | 希望把 Python 嵌入到原生 macOS 应用、或做高度定制化、或提交 App Store | 灵活性最高,可以把 Python 标准库、扩展库、解释器嵌入为 Framework,结合 Objective-C/Swift 代码调用或调度。适合复杂交互或混合开发场景。(Medium) | 学习和工程复杂度高;必须解决签名、公证、架构兼容性、二进制兼容性等多项问题;打包成本大。 |
其它辅助 / 分发工具 | 辅助 .app 打包后的分发、安装体验 | 如用 create-dmg 将 .app 生成 .dmg 、把 .app 打包成 .pkg 、做签名 / 公证 / stapling 等 | 不是单独的“打包 Python -> .app”工具,而是分发链上的补充工具 |
下面我逐个展开讲。
三、py2app:最传统且“mac 本土”的方案
3.1 py2app 介绍与原理
py2app
是一个 Python 包(通常作为 setuptools 的扩展命令),用于把 Python 脚本或包打包成 macOS 的 .app
bundle。它的设计思路类似于 Windows 的 py2exe
:分析你的脚本、收集依赖、复制资源、生成 bundle 结构,并在 .app
包里放入启动器 (stub) 来启动你的代码。(py2app.readthedocs.io)
py2app 支持“alias 模式”(-A
或 --alias
)来构建“指向源代码”的 bundle,用于开发调试,而不是生成完整的独立分发版本。(py2app.readthedocs.io)
但在 “standalone”(独立版本)模式下,会把你的代码、依赖库、Python 运行时一并打包进 .app
。(metachris.com)
3.2 使用示例与基本步骤
下面是一个基于 py2app
打包 GUI 程序(例如使用 Tkinter、PyQt、或其他纯 Python GUI 库)的简单流程。
假设你有一个文件 main.py
,内容是:
import tkinter as tkdef main():root = tk.Tk()root.title("MyApp")tk.Label(root, text="Hello, macOS!").pack()root.mainloop()if __name__ == "__main__":main()
你可以这样打包:
-
在项目中创建
setup.py
:from setuptools import setupAPP = ["main.py"] DATA_FILES = [] # 如果有额外资源,如图标、图片、音频等,放在这里 OPTIONS = {'argv_emulation': True,# 'iconfile': 'app.icns', # 若要自定义图标# 'includes': ['some_module'], # 若有隐式导入 }setup(app=APP,name="MyApp",data_files=DATA_FILES,options={'py2app': OPTIONS},setup_requires=['py2app'], )
-
构建 alias(调试)模式:
python setup.py py2app -A
这种方式构建出来的
.app
并不是完全独立的,仅在当前机器可用,一般用于调试。(py2app.readthedocs.io) -
构建正式版本:
python setup.py py2app
运行后,会生成
dist/MyApp.app
,这是可直接分发的包。(metachris.com) -
测试:在 macOS 上双击
MyApp.app
,看是否能正常启动和运行。 -
若要生成
.dmg
格式分发包,可以在.app
构建成功后,用create-dmg
等工具将.app
打包为.dmg
,让用户通过拖拽安装。(Medium)
资源 / 隐式导入处理:如果你的代码中有动态导入模块、插件路径或使用了非标准路径扫描,你可能需要在
OPTIONS['includes']
、OPTIONS['packages']
、或者OPTIONS['excludes']
中手工指定额外模块,以确保 py2app 能把它们包含进来。
图标:你可以提供
iconfile
选项,指定.icns
图标文件。
资源文件:在
DATA_FILES
中列出你需要打包进入.app
的资源(图片、音频、数据库、配置文件等)。
3.3 优化、注意点与坑
- 对于大型库(如 numpy、scipy、PIL、matplotlib 等),打包后的
.app
体积可能非常大,有时还会在启动时因为库文件或插件路径问题崩溃。许多用户反映这类库在 py2app 打包后容易出问题。(Reddit) - 某些库内部使用 C 扩展或插件机制(如 Qt 插件、动态库路径查找等),py2app 默认的打包逻辑可能无法自动捕获所有需要的
.dylib
或插件,需要你手工配置。 - 你必须在 macOS 环境下运行 py2app 进行打包(不能在 Windows 或 Linux 上打 macOS 应用)。(PyPI)
- 建议在一个干净的 macOS 环境(没有安装你开发时的 Python 库)或虚拟机中测试生成的
.app
,以防“宿主开发环境”中的库被误引用。 - 若你的
.app
未签名 / 未公证,macOS 新版本很可能拒绝启动或警告用户。需要做后面的签名/公证步骤。
总的来说,py2app 是一个相对成熟、社区较为熟悉的方案,但对于复杂依赖可能需要你手动调试。
四、使用 PyInstaller 在 macOS 上生成 .app
bundle
如果你已经熟悉 PyInstaller 并希望在 macOS 上也用它来打包 .app
,这是可行的。PyInstaller 在 macOS 平台下支持生成 BUNDLE(即 .app
)形式。(pyinstaller.org)
4.1 基本命令示例
假设你有 main.py
(GUI 程序),你可以运行:
pyinstaller --windowed --name MyApp --icon app.icns main.py
--windowed
表示这是 GUI 程序,不要在控制台打开终端窗口。--name MyApp
指定输出.app
名称(会生成MyApp.app
)。--icon app.icns
指定应用图标(macOS 图标格式为.icns
)。
执行后,dist
目录中会出现 MyApp.app
。
你也可以在 spec 文件中更细致地控制:
# MyApp.spec
# -*- mode: python ; coding: utf-8 -*-block_cipher = Nonea = Analysis(['main.py'],pathex=[],binaries=[],datas=[],hiddenimports=[],hookspath=[],runtime_hooks=[],excludes=[],win_no_prefer_redirects=False,win_private_assemblies=False,cipher=block_cipher,
)pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)exe = EXE(pyz,a.scripts,[],exclude_binaries=True,name='MyApp',debug=False,bootloader_ignore_signals=False,strip=False,upx=True,console=False,
)coll = COLLECT(exe,a.binaries,a.zipfiles,a.datas,strip=False,upx=True,name='MyApp',
)app = BUNDLE(coll,name='MyApp.app',icon='app.icns',bundle_identifier='com.yourcompany.myapp',info_plist={'CFBundleName': 'MyApp','CFBundleVersion': '0.1.0',},
)
在这个 spec 中,BUNDLE
会把 coll
收集的内容打成 .app
包,你可以指定 bundle_identifier
、info_plist
字段、图标等。(pyinstaller.org)
然后执行:
pyinstaller MyApp.spec
即可生成 MyApp.app
。
4.2 构建 .dmg
分发包
通常你还想把 .app
包做成 .dmg
分发包。一个简单方式是:
-
在命令行安装
create-dmg
(如果你用 Homebrew):brew install create-dmg
-
假设你的
.app
在dist/MyApp.app
,你可以:mkdir dist/dmg cp -R dist/MyApp.app dist/dmg/ create-dmg \--volname "MyApp" \--volicon "app.icns" \--window-pos 200 120 \--window-size 600 300 \--icon-size 100 \--icon "MyApp.app" 175 120 \--hide-extension "MyApp.app" \--app-drop-link 425 120 \dist/MyApp.dmg \dist/dmg/
这个流程在很多教程中常见,也是把 Python GUI 程序打为 macOS 分发包的常见方式。(Medium)
4.3 签名、公证与 Hardened Runtime
对于 macOS 新版本,未签名或未公证 (.notarize) 的 .app
很容易被 Gatekeeper 拒绝或报错。使用 PyInstaller 打包后通常还需进行代码签名和公证处理。下面是常见流程(借鉴社区经验):
-
在打包 spec / BUNDLE 时,在 Info.plist 中设置适当键值(版本、bundle identifier 等)。(Haim Gelfenbeyn’s Blog)
-
对打出来的
.app
做 code signing:codesign -s "Developer ID Application: Your Name (TEAMID)" --deep --timestamp --options runtime "dist/MyApp.app"
这里
--deep
表示对内部所有可执行文件 / 动态库也做签名,--options runtime
启用 Hardened Runtime。(Haim Gelfenbeyn’s Blog) -
创建
.zip
或.dmg
,然后提交给 Apple Notarization 服务:ditto -c -k --keepParent dist/MyApp.app dist/MyApp.zip xcrun altool --notarize-app -t osx -f dist/MyApp.zip --primary-bundle-id com.yourcompany.myapp -u YOUR_APPLE_ID -p APP_SPECIFIC_PASSWORD
提交后等待公证审核。(Haim Gelfenbeyn’s Blog)
-
公证成功后,可以把票据“staple”到
.app
上:xcrun stapler staple dist/MyApp.app
这样用户打开时不再每次联网验证,而是本地携带票据。(Haim Gelfenbeyn’s Blog)
-
最后把
.app
或.dmg
发布给用户。
这个流程虽然有点繁琐,但对于合规分发到 macOS 的普通用户来说几乎是必需的。
五、用 Platypus 封装脚本型应用
如果你的应用比较轻量,可能只是一个命令行脚本或 Python 脚本,不需要复杂 GUI,你可以考虑用 Platypus。
- Platypus 是一个 macOS 工具,可以把脚本(Python、shell、Ruby、Perl 等)包装成
.app
包,在用户双击时以图形界面或后台执行。(Sveinbjörn Þórðarson) - 它支持进度条、脚本输出窗口、拖放文件传参、权限提升等功能。(Sveinbjörn Þórðarson)
- 它适合把脚本包装成便于普通用户使用的「可点击的程序」,但对于复杂的 GUI 程序、重依赖库、C 扩展等场景其处理能力有限。
如果你的项目规模不大,Platypus 是一个值得一试的轻量方案。
六、嵌入 Python 解释器 / 自定义原生应用方式
对于需要最大灵活性、或希望混合使用 Python 与原生 Cocoa / Swift / Objective-C 的场景,你可以把 Python 解释器 / 标准库 /扩展库嵌入到你自己的 macOS 应用工程中。这在某些跨平台框架或苹果平台扩展中常见。中间可能需要用 PythonKit 或自己写桥接代码。(Medium)
优点是你可以在 Xcode 工具链中更细致地控制签名、沙箱权限、资源管理、安全策略等;但缺点是工程复杂度高,需要处理架构兼容(ARM / x86_64)、符号冲突、动态库兼容性、打包流程复杂等。
此外,如果你打算上架 Mac App Store,还需要遵守 Apple 的沙箱、库验证、签名等限制,比如不能使用未经允许的动态库、必须启用 Hardened Runtime、避免容许未签名可执行内存等。嵌入方式通常要更费劲地处理这些问题。(Medium)
七、典型打包流程示例(以 PyInstaller 为例)
下面是一个综合流程示例,假设你有一个 Python GUI 应用 main.py
,想给 mac 用户分发一个 .app
/ .dmg
,具备签名与公证支持。
步骤概要
-
在 macOS 上创建干净环境(如 virtualenv 或干净机器),安装你的应用所需的依赖。
-
在 macOS 上运行 PyInstaller 打包成
.app
:pyinstaller --windowed --name MyApp --icon app.icns main.py
-
在打包选项中通过 spec 文件填充 Info.plist、bundle identifier 等。
-
测试
.app
是否能在 macOS 上正常启动。 -
用
codesign
对.app
签名(包括内部库、插件等)。 -
用
xcrun altool
提交.app
(打包为.zip
或.dmg
)给 Apple 公证服务。 -
stapler staple
将公证票据贴在.app
上。 -
可选:将
.app
放入.dmg
、制作安装体验。 -
最终分发给用户,建议让用户先在干净系统试安装 / 启动。
示例脚本(shell 脚本模拟自动化流程)
下面是一个非常简化的 Bash 脚本骨架,展示从打包到签名与公证的流程:
#!/usr/bin/env bash
set -eAPP_NAME="MyApp"
BUNDLE_ID="com.mycompany.myapp"
ICON_FILE="app.icns"
PYTHON_SCRIPT="main.py"# 1. 清理旧构建
rm -rf build dist# 2. 使用 PyInstaller 生成 .app
pyinstaller --windowed --name "$APP_NAME" --icon "$ICON_FILE" "$PYTHON_SCRIPT"# 3. 签名
codesign -s "Developer ID Application: Your Name (TEAMID)" \--deep --options runtime --timestamp \"dist/${APP_NAME}.app"# 4. 制作 ZIP 或 DMG
ditto -c -k --keepParent "dist/${APP_NAME}.app" "dist/${APP_NAME}.zip"# 5. 提交公证(需提前设置 Apple ID / 密码 / keychain)
xcrun altool --notarize-app -t osx -f dist/${APP_NAME}.zip \--primary-bundle-id "$BUNDLE_ID" -u APPLE_ID -p APP_SPECIFIC_PASSWORD# 6. stapler 把公证票据贴到 .app
xcrun stapler staple "dist/${APP_NAME}.app"echo "Done! You can distribute dist/${APP_NAME}.app (or convert to dmg)."
这个脚本仅为示例。实际中你需要处理的细节很多:检查签名状态、处理签名失败重试、处理异步公证结果 polling、错误日志捕获、公证失败回退策略等。
八、常见问题、坑与调试建议
在把 Python 应用打包为 macOS 应用时,可能会遇很多细节问题,下面给出一些比较常见的坑和应对建议:
-
缺少某些 .dylib 或 插件无法加载
- 使用工具(如
otool -L
、dylibbundler
)检查可执行或库的依赖。 - 在打包工具(py2app / PyInstaller)中显式把缺少的库或插件加入
datas
/binaries
/hiddenimports
。 - 有些库内部对插件或路径做动态查找(如 Qt 插件),你可能得写 hook 脚本或手工拷贝插件目录。
- 使用工具(如
-
签名失败 / 无法公证 / Gatekeeper 拒绝启动
- 确保你有正确的 Developer ID Application 证书,并在 Keychain 中安装好。
- 使用
codesign --deep --options runtime --timestamp
,并签所有子文件。 - 确保你的
.dylib
/.so
/ 扩展模块都已签。 - 公证提交失败 → 检查 Apple 的日志报告,查看可能违反公证规则的库或权限问题。
- 公证通过后使用
stapler staple
把票据贴上。 - 在新版本 macOS 上,某些未签名或未公证的应用一启动就被阻止。
-
启动后崩溃 / 无法加载资源 / 模块未找到
- 在开发阶段先打 “目录” 模式(bundle 模式)而不是压缩或深度打包,查看文件结构是否正确。
- 在
Info.plist
或info_plist
参数中设置正确路径、资源目录、可执行名、CFBundleExecutable 等。 - 打开控制台 (Console.app) 查看 macOS 日志 / 崩溃报告,可能提示缺失库或权限拒绝。
- 在打包时启用调试模式、输出日志、保持调试符号,以便定位问题。
-
架构(Apple Silicon / Intel)不兼容
- 如果你希望支持两种架构(universal 二进制),可能需要分别针对 x86_64 和 arm64 构建,或者用
lipo
/universal2
构建方式合并。 - 某些依赖库可能在某个架构下未编译好,必须先编译支持对应架构再打包。
- 如果你希望支持两种架构(universal 二进制),可能需要分别针对 x86_64 和 arm64 构建,或者用
-
体积过大 / 冗余文件太多
- 检查打包后
.app/Contents/Frameworks
/.app/Contents/Resources
是否包含很多不必要的测试 / 示例 /调试文件。 - 删除不必要模块,使用
--exclude
或excludes
选项。 - 对资源文件做压缩、剔除未用资源。
- 检查打包后
九、总结与建议
- 对于多数纯 Python GUI 程序,py2app 是 macOS 平台上最“本地化”的选择,社区支持也比较成熟。
- 如果你已经使用 PyInstaller 在 Windows 或 Linux 上打包,并希望代码分发逻辑一致,可以考虑用 PyInstaller 的
BUNDLE
模式在 macOS 上打包应用。 - 无论用哪种工具,最终要面对的都是 macOS 的签名、公证、架构兼容、动态库依赖等问题。
- 在打包过程中,一定要在干净环境或虚拟机里做最终测试,不能只在开发机上验证。
- 对于“桌面级”应用,做好
.dmg
/.pkg
的用户体验以及签名 / 公证是关键一环。 - 如果对嵌入式或混合场景有需求(例如你的应用用 Swift / Cocoa 与 Python 混合),可以考虑把 Python 嵌入到原生应用中,但那条路比较复杂。