Jason Pan

PyInstaller 打包步骤与常见问题

黄杰 / 2021-06-01


对于 Python 应用而言,直接部署源代码,会依赖于不同版本的 Python 以及第三方的模块,需要繁杂的依赖环境搭建。

使用 PyInstaller 构建出的独立应用(可以是单一文件),依赖于系统库,但是不依赖于 Python 版本和第三方库,非常有利于部署。

本节简单介绍构建过程(针对一个完整的 package),并记录使用过程中遇到的问题和解决方法。

构建过程

针对已经存在的 Python 应用项目,将代码拉到一个特定系统(或使用容器)中,进行打包的步骤:

  1. 安装对应版本的 Python,能运行项目的 Python 版本
  2. 安装 PyInstaller
  3. 安装 pipreqs
  4. 拉取项目代码,并切换路径
  5. 利用 pipreqs 确认依赖,得到 requirements.txt 文件
  6. 根据 requirement.txt 安装所需依赖模块
  7. 使用 pyinstaller 构建包或独立文件
  8. 得到的文件即可在其他机器上运行

上述过程转换成代码则是:

yum install python3
python3 -m pip install pyinstaller pipreqs
git clone http://github.com/user/xxx
cd xxx
pipreqs ./ --encoding=utf-8 --force
python3 -m pip install -r requirements.txt
python3 -m PyInstaller x1/x2/xxx.py --clean --onefile

打包完之后可以在 ./dist/ 目录下看到刚才产生的文件。使用 file 可以查看文件类型,使用 ldd 可以查看依赖的动态库。

构造规范的包

Python 只把含 __init__.py 文件的目录当成包。如果有通过点分的方式引用自定义的包,则对应的包中必须要创建对应的__init__.py。不然可能会遇到以下这种错误:

Traceback (most recent call last):
  File "/usr/lib64/python3.6/site.py", line 73, in <module>
    import os
...
AttributeError: module 'os' has no attribute 'path'
Failed to import the site module

使用 -p 指定包含目录

假设我们的项目目录是 /data/sacc,直接执行其中一个模块的时候,需要这样调用:

PYTHONPATH=$PWD python3 apps/blacklist/console_tools.py

不带 PYTHONPATH 的话,会报错:

Traceback (most recent call last):
  File "apps/blacklist/console_tools.py", line 4, in <module>
    from apps.blacklist.prome_exporter import main as prome_exporter_main
ModuleNotFoundError: No module named 'apps

如果在项目的根目录中,直接调用 pyinstaller 是能够找到相应模块的。

但是如果要在别的目录中调用 pyinstaller 则需要通过 -p 选项来指定查找目录,作用类似于 PYTHONPATH

比如我们在 /data/ 目录下构建需要执行以下代码(如果不使用-p 可以成功构建,运行时同样报 ModuleNotFoundError 的错误):

pyintaller --onefile --clean sacc/apps/blacklist/console_tools.py -p sacc/

idna 未知编码错误

如果用到网络相关的库,很可能你会遇到 LookupError: unknown encoding: idna 这个问题:

Traceback (most recent call last):
  ...
  File "socket.py", line 693, in create_connection
  File "socket.py", line 732, in getaddrinfo
LookupError: unknown encoding: idna

原因是 socket.getaddrinfo() 函数会将 unicode 的 hostnames 使用 idna 进行编码,触发这个错误。PyInstaller 项目的 Issue #1113 有讨论到两种方法来避免这一个问题:

卸载 enum34

需要卸载 enum34 模块,不然可能会报如下错误:

Traceback (most recent call last):
  File "<string>", line 4, in <module>
  File "/usr/lib64/python3.6/traceback.py", line 5, in <module>
    import linecache
  File "/usr/lib64/python3.6/linecache.py", line 11, in <module>
    import tokenize
  File "/usr/lib64/python3.6/tokenize.py", line 33, in <module>
    import re
  File "/usr/lib64/python3.6/re.py", line 142, in <module>
    class RegexFlag(enum.IntFlag):
AttributeError: module 'enum' has no attribute 'IntFlag'

合并多个模块

如果有多个模块需要打包,可以放一个独立的文件,将所有的模块都引入,然后根据不同的参数来区分使用哪个功能:

# -*- encoding: utf-8 -*-
import argparse
from apps.blacklist.prome_exporter import main as prome_exporter_main
from apps.blacklist.stat import main as stat_main
from apps.blacklist.updater import main as updater_main

if __name__ == "__main__":
    u''.encode('idna')
    parser = argparse.ArgumentParser()
    parser.add_argument("action",  choices=['stat', 'exporter', 'updater'],help="")
    args = parser.parse_known_args()
    app = {
        'stat': stat_main,
        'exporter': prome_exporter_main,
        'updater': updater_main,
    }.get(args[0].action)
    if len(args[1]) > 0:
      app(args[1])
    else:
      app()

P.S. parse_known_args() 可以将多余的参数储存起来,以供子模块解析。

启动进程

通过 pyinstaller 产生的单个可执行文件,拉起后会产生两个进程:

user00   31362     1  0 02:30 ?        00:00:00 ./console_tools stat
user00   31363 31362  4 02:30 ?        00:04:38 ./console_tools stat

PyInstaller 项目的 Issue #2483 对此有解释:

When you build in onefile mode (-F), the program is decompressed to a temporary directory and run from there. The second process is your actual program, while the first process is meant to clean up the temporary directory after the program exits or crashes.

If there were only one process, there would be no way to clean up the temp directory in the event of a crash.

构建单个可执行文件的模式中,第一个进程是将程序解压到临时文件夹,并在此调用第二个进程。第二个进程才是真正的程序,当真正的程序退出或者崩溃之后,第一个进程会清理临时文件夹。

如果只有一个文件夹,将无法在真正程序崩溃的时候清理临时文件加。

TODO: 提供一个可供练习的 Python 项目