Qt开发-QT项目程序打包pyinstaller

pyinstaller

这个打包库最大的好处是

会自动分析项目的依赖,比较智能方便。

其他的诸如cx_Freezepy2exenuitka都不是开箱即用的,都不如pyinstaller智能,这里就不再推荐。

查看版本

1
pyinstaller --version

卸载

1
pip uninstall pyinstaller

安装pyinstaller

1
pip install pyinstaller==6.11.1

Win上打包

1
pyinstaller main.py -y --windowed

设置ico

1
pyinstaller main.py -y --noconsole --name="xh-marking-client" --icon="logo.ico"

单文件

1
pyinstaller main.py -y -F --noconsole --name="xh-marking-client" --icon="logo.ico"

注意

在Win上--windowed--noconsole作用一样,但是--windowed只能在Win上用,--noconsole还可以在Linux上打包。

关于打包报毒

每次打包的时候,Windows 安全中心都报毒,生成的EXE文件就被删了,经过测试发现这个误报只会生成的时候误报,所以我们设置例外就行了。

实测生成EXE后在其他设备上也是不会再误报了。

常见问题

pyzbar的DLL没有正常导入

pyinstaller 可能没有正确检测到 pyzbar 的隐藏导入,导致打包后程序找不到相关模块。

先运行一次打包生成xh-marking-client.spec文件

1
pyinstaller main.py -y --noconsole --name="xh-marking-client" --icon="logo.ico"

修改xh-marking-client.spec如下部分datas中的内容

1
2
3
4
5
6
7
8
9
10
11
12
a = Analysis(
datas=[
(
r'D:\Project\qt\xh-marking-client-qt\.env382_32\Lib\site-packages\pyzbar\libzbar-32.dll',
r'.\pyzbar'
),
(
r'D:\Project\qt\xh-marking-client-qt\.env382_32\Lib\site-packages\pyzbar\libiconv-2.dll',
r'.\pyzbar'
)
],
)

重新打包

1
pyinstaller -y xh-marking-client.spec

DLL引用

添加项目下dll

1
pyinstaller main.py -y --noconsole --name="xh-marking-client" --icon="logo.ico" --add-binary "dll\twain\32\TWAINDSM.dll;."

上面这种方式TWAINDSM.dll会放在_internal目录中,这样虽然路径变了,但是_internal根路径的DLL因为可以正常加载,所以可以正常使用。

打包添加文件夹

1
pyinstaller main.py -y --noconsole --name="xh-marking-client" --icon="logo.ico" -D --add-binary "dll;dll"

上面这种方式dll文件夹会放在_internal目录中,这样路径变了,根据路径加载的dll会无法加载。

其中

-D--onedir 选项时,PyInstaller 会把所有依赖项和资源文件组织到一个单独的目录中,而不是将它们打包进一个单一的可执行文件里。

该目录会包含主可执行文件以及程序运行所需的所有支持文件,像动态链接库(DLL)、Python 运行时库、数据文件等。

添加配置和图标

自 Pyinstaller>=6.0.0 版本后,在打包 one dir(-D 目录模式)时,除可执行文件外,其余文件都将被转移到 _internal 文件夹下

并且添加文件的时候我们也不能使用类似这样的写法--add-data "config.ini;.."来达到放在exe同级的目的,会报错

1
pyinstaller main.py -y --noconsole --name="xh-marking-client" --icon="logo.ico" -D --add-data "*.ini;." --add-data "logo.ico;."

这会导致添加的配置文件和exe不在同级目录,读取配置会有问题

可以添加--contents-directory .参数恢复到之前的模式(注意此参数生效需要 pyinstaller>=6.1.0

1
pyinstaller main.py -y --noconsole --name="xh-marking-client" --icon="logo.ico" --contents-directory . --add-data "config_app.ini;." --add-data "logo.ico;."

另一种方法是降低pyinstaller版本,pyinstaller < 6.0.0

过滤DLL

PyInstaller 打包基本流程

PyInstaller 的打包过程主要包含以下关键阶段:

  1. 分析阶段(Analysis):PyInstaller 会分析主脚本及其依赖项,确定需要打包的所有 Python 模块、二进制文件(像 DLL)、数据文件等。此阶段会构建一个依赖图,明确各文件间的依赖关系。
  2. 收集阶段(Collection):基于分析阶段的结果,PyInstaller 会收集所有必需的文件,包含 Python 模块、DLL 文件、数据文件等,并将它们复制到临时目录。在这个阶段,DLL 文件会被复制到相应位置。
  3. 打包阶段(Packaging):将收集到的文件打包成一个或多个可执行文件,这可能涉及将文件压缩、加密等操作。
  4. 运行时钩子执行阶段:当运行打包生成的可执行文件时,--runtime-hooks 指定的钩子文件会在 Python 解释器启动之后、主脚本执行之前运行。

方式1

打包,同时会自动生成spec文件

1
pyinstaller main.py -y --noconsole --name="xh-marking-client" --icon="logo.ico"

修改spec文件

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# -*- mode: python ; coding: utf-8 -*-

a = Analysis(
["main.py"],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)

exclude_dlls = ["Qt5WebEngineCore.dll"]
# 过滤掉要排除的DLL文件
filtered_binaries = []
for binary in a.binaries:
if not any(exclude_dll in binary[0] for exclude_dll in exclude_dlls):
filtered_binaries.append(binary)
a.binaries = filtered_binaries

pyz = PYZ(a.pure)

exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name="xh-marking-client",
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=["logo.ico"],
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name="xh-marking-client",
)

使用修改后的spec文件打包

1
pyinstaller xh-marking-client.spec --clean -y

方式2(无效)

注意

这种方式我测试的没有效果,应该时钩子执行了,但是PySide2还有其他的钩子,执行顺序的原因导致没有效果。

创建一个钩子文件来排除指定的 DLL 文件。

步骤:

创建钩子文件:在项目目录下创建一个名为 hook-PySide2.py 的文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# hook-PySide2.py
from PyInstaller.utils.hooks import collect_dynamic_libs

# 定义要排除的 DLL 列表
exclude_dlls = ["Qt5WebEngineCore.dll"]


def pre_collect_dynamic_libs(collector):
# 获取 PySide2 的动态库列表
libs = collect_dynamic_libs("PySide2")
print("排除之前:", len(libs))
# 过滤掉要排除的 DLL
filtered_libs = [
lib
for lib in libs
if not any(exclude_dll in lib[1] for exclude_dll in exclude_dlls)
]
# 更新收集器中的动态库列表
collector.dynamic_libs = filtered_libs

使用钩子文件进行打包

1
pyinstaller --additional-hooks-dir=. main.py -y --noconsole --name="xh-marking-client" --icon="logo.ico"

添加DLL

先运行

1
pyinstaller main.py -y --noconsole --name="xh-marking-client" --icon="logo.ico" --contents-directory . --add-data "*.ini;." --add-data "logo.ico;."

这会生成xh-marking-client.spec文件

修改该文件

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
a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=[
('config_app.ini', '.'),
('logo.ico', '.'),
(
r'D:\Project\python\xh-marking-client-qt\.venv\Lib\site-packages\pyzbar\libzbar-32.dll',
r'.\pyzbar'
),
(
r'D:\Project\python\xh-marking-client-qt\.venv\Lib\site-packages\pyzbar\libiconv-2.dll',
r'.\pyzbar'
)
],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)

修改后以后打包都使用下面的的命令

1
pyinstaller -y xh-marking-client.spec

整体配置

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# -*- mode: python ; coding: utf-8 -*-


a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=[
('config_app.ini', '.'),
('logo.ico', '.'),
(
r'D:\Project\python\xh-marking-client-qt\.venv\Lib\site-packages\pyzbar\libzbar-32.dll',
r'.\pyzbar'
),
(
r'D:\Project\python\xh-marking-client-qt\.venv\Lib\site-packages\pyzbar\libiconv-2.dll',
r'.\pyzbar'
)
],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)

exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='xh-marking-client',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=['logo.ico'],
contents_directory='.',
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='xh-marking-client',
)