把 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](https://sveinbjorn.org/platypus/ "Platypus - Create Mac apps from command line scripts "))
嵌入 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,内容是:

python 复制代码
import tkinter as tk

def main():
    root = tk.Tk()
    root.title("MyApp")
    tk.Label(root, text="Hello, macOS!").pack()
    root.mainloop()

if __name__ == "__main__":
    main()

你可以这样打包:

  1. 在项目中创建 setup.py:

    python 复制代码
    from setuptools import setup
    
    APP = ["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'],
    )
  2. 构建 alias(调试)模式:

    bash 复制代码
    python setup.py py2app -A

    这种方式构建出来的 .app 并不是完全独立的,仅在当前机器可用,一般用于调试。(py2app.readthedocs.io)

  3. 构建正式版本:

    bash 复制代码
    python setup.py py2app

    运行后,会生成 dist/MyApp.app,这是可直接分发的包。(metachris.com)

  4. 测试:在 macOS 上双击 MyApp.app,看是否能正常启动和运行。

  5. 若要生成 .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 程序),你可以运行:

bash 复制代码
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 文件中更细致地控制:

python 复制代码
# MyApp.spec
# -*- mode: python ; coding: utf-8 -*-

block_cipher = None

a = 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)

然后执行:

bash 复制代码
pyinstaller MyApp.spec

即可生成 MyApp.app。

4.2 构建 .dmg 分发包

通常你还想把 .app 包做成 .dmg 分发包。一个简单方式是:

  1. 在命令行安装 create-dmg(如果你用 Homebrew):

    bash 复制代码
    brew install create-dmg
  2. 假设你的 .app 在 dist/MyApp.app,你可以:

    bash 复制代码
    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 打包后通常还需进行代码签名和公证处理。下面是常见流程(借鉴社区经验):

  1. 在打包 spec / BUNDLE 时,在 Info.plist 中设置适当键值(版本、bundle identifier 等)。(Haim Gelfenbeyn's Blog)

  2. 对打出来的 .app 做 code signing:

    bash 复制代码
    codesign -s "Developer ID Application: Your Name (TEAMID)" --deep --timestamp --options runtime "dist/MyApp.app"

    这里 --deep 表示对内部所有可执行文件 / 动态库也做签名,--options runtime 启用 Hardened Runtime。(Haim Gelfenbeyn's Blog)

  3. 创建 .zip 或 .dmg,然后提交给 Apple Notarization 服务:

    bash 复制代码
    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)

  4. 公证成功后,可以把票据"staple"到 .app 上:

    bash 复制代码
    xcrun stapler staple dist/MyApp.app

    这样用户打开时不再每次联网验证,而是本地携带票据。(Haim Gelfenbeyn's Blog)

  5. 最后把 .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,具备签名与公证支持。

步骤概要

  1. 在 macOS 上创建干净环境(如 virtualenv 或干净机器),安装你的应用所需的依赖。

  2. 在 macOS 上运行 PyInstaller 打包成 .app:

    bash 复制代码
    pyinstaller --windowed --name MyApp --icon app.icns main.py
  3. 在打包选项中通过 spec 文件填充 Info.plist、bundle identifier 等。

  4. 测试 .app 是否能在 macOS 上正常启动。

  5. 用 codesign 对 .app 签名(包括内部库、插件等)。

  6. 用 xcrun altool 提交 .app(打包为 .zip 或 .dmg)给 Apple 公证服务。

  7. stapler staple 将公证票据贴在 .app 上。

  8. 可选:将 .app 放入 .dmg、制作安装体验。

  9. 最终分发给用户,建议让用户先在干净系统试安装 / 启动。

示例脚本(shell 脚本模拟自动化流程)

下面是一个非常简化的 Bash 脚本骨架,展示从打包到签名与公证的流程:

bash 复制代码
#!/usr/bin/env bash
set -e

APP_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 应用时,可能会遇很多细节问题,下面给出一些比较常见的坑和应对建议:

  1. 缺少某些 .dylib 或 插件无法加载

    • 使用工具(如 otool -L、dylibbundler)检查可执行或库的依赖。
    • 在打包工具(py2app / PyInstaller)中显式把缺少的库或插件加入 datas / binaries / hiddenimports。
    • 有些库内部对插件或路径做动态查找(如 Qt 插件),你可能得写 hook 脚本或手工拷贝插件目录。
  2. 签名失败 / 无法公证 / Gatekeeper 拒绝启动

    • 确保你有正确的 Developer ID Application 证书,并在 Keychain 中安装好。
    • 使用 codesign --deep --options runtime --timestamp,并签所有子文件。
    • 确保你的 .dylib / .so / 扩展模块都已签。
    • 公证提交失败 → 检查 Apple 的日志报告,查看可能违反公证规则的库或权限问题。
    • 公证通过后使用 stapler staple 把票据贴上。
    • 在新版本 macOS 上,某些未签名或未公证的应用一启动就被阻止。
  3. 启动后崩溃 / 无法加载资源 / 模块未找到

    • 在开发阶段先打 "目录" 模式(bundle 模式)而不是压缩或深度打包,查看文件结构是否正确。
    • 在 Info.plist 或 info_plist 参数中设置正确路径、资源目录、可执行名、CFBundleExecutable 等。
    • 打开控制台 (Console.app) 查看 macOS 日志 / 崩溃报告,可能提示缺失库或权限拒绝。
    • 在打包时启用调试模式、输出日志、保持调试符号,以便定位问题。
  4. 架构(Apple Silicon / Intel)不兼容

    • 如果你希望支持两种架构(universal 二进制),可能需要分别针对 x86_64 和 arm64 构建,或者用 lipo / universal2 构建方式合并。
    • 某些依赖库可能在某个架构下未编译好,必须先编译支持对应架构再打包。
  5. 体积过大 / 冗余文件太多

    • 检查打包后 .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 嵌入到原生应用中,但那条路比较复杂。
相关推荐
默_笙3 天前
🍙 给每个请求过安检:FastAPI 是怎么把校验写进类型注解的
python
qq_426003963 天前
启动playwright录制codegen生成自动化测试脚本
python·自动化
虎头金猫3 天前
4K 视频总卡在公网带宽?用 N1 + OpenList 把网盘播放链路重新理顺
运维·服务器·网络·python·容器·beautifulsoup·pandas
长沙三为智能科技3 天前
家政小程序开发从0到上线:五阶段交付流程与验收清单
python
伞伞悦读3 天前
【第38期】Python 模块与包详解:import、from、模块搜索路径、包结构和 __init__
开发语言·python
只睡四小时3 天前
Canvas 弹道联机实战:700 行 + 固定时间步长
python·websocket·html5·游戏开发·canvas
奇思妙想聪明勤奋的小羊3 天前
DeepAgents第5章:子Agent 与上下文隔离—让 Agent学会委派
人工智能·python·学习·语言模型
lpfasd1233 天前
2026年第38周GitHub趋势周报
python·科技·github
IZero073 天前
Jev 与 Laya
python·语言模型