Skip to content

商务合作:vTikTok


公众号:



Python程序的打包


pyinstaller和py2app都可以用来打包Python程序,pyinstaller可以打包为任意平台的软件,但在MAC电脑上,更推荐使用py2app进行打包。

不过在使用py2app打包的时候,遇到不少的坑,记录一下。

打包使用venv环境

在进行Python相关的开发,我更多使用的是conda,但是在conda环境下打包,总是报一个奇怪的错误:

Alt text

查阅很多资料,都无法解决,后来更换为venv后,才正常运行。

如何排查这种错误呢?

右键->显示包内容->MacOS->双击xx软件名,这样可以在终端运行,可以显示出错误的原因:

ImportError: dlopen(/Users/alien/PycharmProjects/PythonTest/dist/test.app/Contents/Resources/lib/python3.9/lib-dynload/_ctypes.so, 0x0002): Library not loaded: @rpath/libffi.8.dylib
ImportError: dlopen(/Users/alien/PycharmProjects/PythonTest/dist/test.app/Contents/Resources/lib/python3.9/lib-dynload/_ctypes.so, 0x0002): Library not loaded: @rpath/libffi.8.dylib

很明显是缺少某些库依赖导致的,这明显是没有把Python环境的某些库正常打包进来,网上有些解决方案是手动移动库到包里面,不建议,换用venv环境重新打包即可。

安装依赖

requirements.txt

python
py2app
flask
pywebview
py2app
flask
pywebview

这里用到了py2app,flask和pywebview,所以要先安装相关依赖。

生成setup.py文件

py2applet --make-setup 程序名称.py

这会生成一个默认的setup.py

python
"""
This is a setup.py script generated by py2applet

Usage:
    python setup.py py2app
"""

from setuptools import setup

APP = ['main.py']
DATA_FILES = []
OPTIONS = {}

setup(
    app=APP,
    data_files=DATA_FILES,
    options={'py2app': OPTIONS},
    setup_requires=['py2app'],
)
"""
This is a setup.py script generated by py2applet

Usage:
    python setup.py py2app
"""

from setuptools import setup

APP = ['main.py']
DATA_FILES = []
OPTIONS = {}

setup(
    app=APP,
    data_files=DATA_FILES,
    options={'py2app': OPTIONS},
    setup_requires=['py2app'],
)

打包

python setup.py py2app

python setup.py py2app -A

添加 -A 参数进行测试打包,速度较快,但是不会将依赖打包进去。

排坑时刻一

我的打包应用是基于Vue3 + Flask + pywebview + py2app 打包的,而这个py2app打包远没有pyinstaller顺利。

我的完整setup.py的配置,各位可以直接拿去用:

python
"""
This is an example of py2app setup.py script for freezing your pywebview
application

Usage:
    python setup.py py2app
"""

import os

from setuptools import setup

ENTRY_POINT = ['main.py']

# 自写模块放在DATA_FILES列表中
DATA_FILES = []
dest_dir = "dist/main.app/Contents/Resources/"

# 第三方库放在OPTIONS下的includes对应的列表中
OPTIONS = {
    'argv_emulation': False,
    'strip': False,  # 不去除debug信息
    'iconfile': 'logo.icns',  # 应用图标
    'includes': ['WebKit', 'Foundation', 'webview']  # 需要打包的第三方库
}

setup(
    app=ENTRY_POINT,
    data_files=DATA_FILES,
    options={'py2app': OPTIONS},
    setup_requires=['py2app'],
)

# -rf 是cp命令的选项,分别表示递归复制、保留文件属性和保留目录结构。
os.system("cp -rf h5 " + dest_dir + "h5")
os.system("cp -rf server.py " + dest_dir + "server.py")
os.system("cp -rf api.py " + dest_dir + "api.py")
os.system("cp -rf app.py " + dest_dir + "app.py")
os.system("cp -rf logo.icns " + dest_dir + "logo.icns")
"""
This is an example of py2app setup.py script for freezing your pywebview
application

Usage:
    python setup.py py2app
"""

import os

from setuptools import setup

ENTRY_POINT = ['main.py']

# 自写模块放在DATA_FILES列表中
DATA_FILES = []
dest_dir = "dist/main.app/Contents/Resources/"

# 第三方库放在OPTIONS下的includes对应的列表中
OPTIONS = {
    'argv_emulation': False,
    'strip': False,  # 不去除debug信息
    'iconfile': 'logo.icns',  # 应用图标
    'includes': ['WebKit', 'Foundation', 'webview']  # 需要打包的第三方库
}

setup(
    app=ENTRY_POINT,
    data_files=DATA_FILES,
    options={'py2app': OPTIONS},
    setup_requires=['py2app'],
)

# -rf 是cp命令的选项,分别表示递归复制、保留文件属性和保留目录结构。
os.system("cp -rf h5 " + dest_dir + "h5")
os.system("cp -rf server.py " + dest_dir + "server.py")
os.system("cp -rf api.py " + dest_dir + "api.py")
os.system("cp -rf app.py " + dest_dir + "app.py")
os.system("cp -rf logo.icns " + dest_dir + "logo.icns")

使用os.system的方式直接把文件拷贝到包目录下,相比配置一些文件要简单的多,官方的案例提供了一个方法并不易用。

排坑时刻二

防坑指南:注意pyinstaller和py2app打包路径的不同,如果配置错误,会找不到h5文件夹。

python
h5_folder = os.path.join(os.getcwd(), 'h5')
print("MAC执行H5路径 = ", h5_folder)

server = Flask(__name__, static_folder=h5_folder, static_url_path="/", template_folder=None)
server.config['SEND_FILE_MAX_AGE_DEFAULT'] = 1  # disable caching
h5_folder = os.path.join(os.getcwd(), 'h5')
print("MAC执行H5路径 = ", h5_folder)

server = Flask(__name__, static_folder=h5_folder, static_url_path="/", template_folder=None)
server.config['SEND_FILE_MAX_AGE_DEFAULT'] = 1  # disable caching