Pyinstaller打包Pyd文件到exe(update)

前言

去年写了一篇文章,就是使用 Pyinstaller 编译 pyd 文件到 exe 中的,只不过上次是记录了解决问题的过程,并没有详细记录打包的过程和原理,这次正好借用新项目来记录下。

项目介绍

项目中用 Python 开发了一款数据采集工具,部署端为 Windows 服务器,为了在服务器上直接运行,需要将程序打包为 exe 格式。为了保护源代码并且加快程序的运行速度,使用 Cython 编译项目各个模块,使用 Pyinstaller 将模块打包为 exe 文件。

项目结构

项目所有代码均为异步,使用到了 asyncio 和一个异步的 opc client,和 PLC 进行交互,项目结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
D:\AQ
│ .gitignore
│ 1.txt
│ build2pyd.bat
│ config.ini
│ main.py
│ README.md
│ requirements.txt
│ setup.py
│ start.bat
│ test.py

├─aq
│ alarm.py
│ andon.py
│ andon_interactive.py
│ device_data.py
│ mes.py
│ opc.py
│ sql.py
│ util.py
│ __init__.py

其中,main.py 为启动文件,aq 为一个 Python 包,所有的业务逻辑都在 aq 包中,只需要将包中所有文件编译为 pyd 文件,之后使用 Pyinstaller 打包即可。

打包思路

打包思路为:通过 Cython 编译 aq 文件夹中所有的文件为 pyd 文件,然后删除对应的 py 文件,在 import 时就可以直接导入 pyd 了。然后使用 Pyinstaller 打包所有的文件为 exe,使用 --hidden-import 参数添加依赖项,使用 --add-data 参数添加所有的 pyd 文件到程序中,最后进行清理工作。

这里涉及到一个问题就是:Pyinstaller 会自动分析 py 文件,然后将需要导入的依赖自动导入到程序中去,但是由于我们已经将 py 文件编译为 pyd 文件了,所以 Pyinstaller 无法分析编译后的二进制 pyd 文件,导致不能自动导入依赖到程序中。

解决此问题的方法就是我们需要使用 --hidden-import 参数来手动指定所有的依赖。

编译脚本

编译脚本(setup.py)为:

1
2
3
4
5
6
7
8
9
10
from distutils.core import setup

from Cython.Build import cythonize

setup(
name='aq app',
ext_modules=cythonize(module_list="aq/*.py", exclude=['aq/__init__.py'], # 排除__init__.py文件
language_level="3str", # 使用python3版本的字符串格式
compiler_directives={'always_allow_keywords': True} # 允许使用关键字参数
))

打包脚本

打包脚本(build2pyd.bat)为:

1
2
3
4
5
6
7
8
9
10
11
python setup.py build_ext --inplace
mkdir temp
move aq\*.pyd temp
del aq\*.c
rename aq aaa
pyinstaller -F main.py --hidden-import logging.handlers --hidden-import asyncua --distpath ./ --clean --add-data ./temp;aq
del main.spec
rename aaa aq
rmdir /s /q temp
rmdir /s /q build
rmdir /s /q __pycache__

打包流程

  • 先运行 setup.py,编译所有的 py 文件为 pyd 文件
  • 然后新建一个临时文件夹, 将所有的 pyd 文件移动到临时文件夹中
  • 删除原有文件夹中所有的 c 文件,重命名 aq 文件夹为一个其他名字,然后执行打包脚本
  • 在打包时 Pyinstaller 会根据 main.py 中的 import 信息自动去对应文件夹导入 py 文件,为了防止 py 文件被导入需要将 aq 文件夹重命名。
  • 打包时比较重要的两个参数为 --hidden-import--add-data,第一个参数用来导入依赖项,因为 py 文件被打包为 pyd 文件了,Pyinstaller 无法判断 pyd 文件中导入了什么其他依赖,所以默认不会导入 pyd 所需要的依赖,需要使用 --hidden-import 参数手动导入对应依赖。第二个参数用来将编译好的 pyd 文件添加到打包的文件中,并且将 temp 重命名为 aq,因为 main.py 中所有内容原本就是从 aq 包中导入的,无论包中是 py 文件还是 pyd 文件,包名要保持相同。
  • 删除一些无用的文件,将文件夹的名字恢复为 aq,结束脚本。

持续集成

以上过程可以通过持续集成系统变成完全的自动化流程,这里我使用 GitHub Actions 来实现自动编译和打包过程,配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
name: build Python Package

on: push

jobs:
deploy:

runs-on: windows-latest

steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.8.6'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools
cd aq && pip install -r requirements.txt
- name: Build
run: |
git clone --depth=1 https://github.com/cython/cython.git && cd cython && python setup.py install
cd .. && cd aq && cmd.exe /K build2pyd.bat
dir
- name: Archive production artifacts
uses: actions/upload-artifact@v2
with:
name: main
path: aq/main.exe
retention-days: 3

参考官方文档可以很快写出此配置文件,仅供参考。

这里比较特殊的是 Cython 依赖,由于目前最新版本的 Cython 不支持项目中用到的 iscoroutinefunction 函数,详细信息可以看这里,这里需要自己编译安装 Cython 依赖。

总结

打包过程到这里就结束了,总结下整体的流程,其实就两个,编译和打包,编译基本上一条命令就可以搞定,打包的问题稍微多一点,如果有问题的话可以看看我之前的一篇文章

参考链接

https://blog.d77.xyz/archives/75cf9cb3.html

本文章首发于个人博客 LLLibra146’s blog
本文作者:LLLibra146
版权声明:本博客所有文章除特别声明外,均采用 © BY-NC-ND 许可协议。非商用转载请注明出处!严禁商业转载!
本文链接https://blog.d77.xyz/archives/118d44fa.html