python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python应用打包成macOS应用

将Python应用打包成macOS应用的详细步骤

作者:迎风斯黄

这篇文章主要介绍了在macOS上打包Python应用的挑战与注意事项,包括使用py2app、PyInstaller、Platypus和嵌入Python解释器等工具和方案,文章详细讲解了每个工具的使用方法、优缺点以及在打包过程中需要注意的问题,需要的朋友可以参考下

一、打包 macOS 应用的挑战与注意事项

在 macOS 上打包 Python 应用,和在 Windows 上打包有不少相似点(都要把 Python 运行时 + 依赖打包进来),但也有自己独特的挑战:

因此,在工具和流程选择时,需要兼顾“可靠性”“分发体验”“签名/公证支持”等多个维度。

下面我先介绍几种主流工具/方案,然后给出一个典型流程与调试建议。

二、常用工具 / 方案对比

以下是打包 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 tk

def 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 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'],
)

构建 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)

3.3 优化、注意点与坑

总的来说,py2app 是一个相对成熟、社区较为熟悉的方案,但对于复杂依赖可能需要你手动调试。

四、使用 PyInstaller 在 macOS 上生成.appbundle

如果你已经熟悉 PyInstaller 并希望在 macOS 上也用它来打包 .app,这是可行的。PyInstaller 在 macOS 平台下支持生成 BUNDLE(即 .app)形式。(pyinstaller.org)

4.1 基本命令示例

假设你有 main.py(GUI 程序),你可以运行:

pyinstaller --windowed --name MyApp --icon app.icns main.py

执行后,dist 目录中会出现 MyApp.app

你也可以在 spec 文件中更细致地控制:

# 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_identifierinfo_plist 字段、图标等。(pyinstaller.org)

然后执行:

pyinstaller MyApp.spec

即可生成 MyApp.app

4.2 构建.dmg分发包

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

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

brew install create-dmg

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

  1. 在打包 spec / BUNDLE 时,在 Info.plist 中设置适当键值(版本、bundle identifier 等)。(Haim Gelfenbeyn’s Blog)
  2. 对打出来的 .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 是一个值得一试的轻量方案。

六、嵌入 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
pyinstaller --windowed --name MyApp --icon app.icns main.py
  1. 在打包选项中通过 spec 文件填充 Info.plist、bundle identifier 等。
  2. 测试 .app 是否能在 macOS 上正常启动。
  3. codesign.app 签名(包括内部库、插件等)。
  4. xcrun altool 提交 .app(打包为 .zip.dmg)给 Apple 公证服务。
  5. stapler staple 将公证票据贴在 .app 上。
  6. 可选:将 .app 放入 .dmg、制作安装体验。
  7. 最终分发给用户,建议让用户先在干净系统试安装 / 启动。

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

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

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

签名失败 / 无法公证 / Gatekeeper 拒绝启动

启动后崩溃 / 无法加载资源 / 模块未找到

架构(Apple Silicon / Intel)不兼容

体积过大 / 冗余文件太多

九、总结与建议

以上就是将Python应用打包成macOS应用的详细步骤的详细内容,更多关于Python应用打包成macOS应用的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文