ZB100ImageGenerator.spec
4.27 KB
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# -*- mode: python ; coding: utf-8 -*-
"""
PyInstaller spec for ZB100ImageGenerator.
跨平台构建配置的唯一真相源 (macOS + Windows)。
build_mac_universal.sh / build_windows.bat 只负责环境准备,
实际构建调用 `pyinstaller ZB100ImageGenerator.spec`。
修复点:
- macOS 上 PIL/_imaging.so 依赖 @rpath/libtiff.6.dylib 等原生库,
PyInstaller 把 @rpath 改写成 @loader_path/.. (= Contents/Frameworks/),
因此 .dylibs/*.dylib 必须平铺到 bundle 根目录。
部署坑 (不是打包问题, 但会让人误以为打包挂了):
- PyInstaller 6.x 会把 PIL/.dylibs 编码成 PIL/__dot__dylibs 目录,
再建一个 PIL/.dylibs -> __dot__dylibs 的符号链接.
- Frameworks/libtiff.6.dylib 又通过符号链接指向 PIL/.dylibs/libtiff.6.dylib,
是**两层嵌套 symlink**.
- 走 NAS (某些 SMB 挂载) / 邮件 / 某些解压工具传 .app 时, 内层 symlink
可能被吞掉, 导致打开时 dlopen 找不到 libtiff.6.dylib.
- 传输务必用 `tar czf` 或 `rsync -a`, 别用 cp / Finder 拖拽过 NAS.
策略 (三重保险):
1. PyInstaller 官方工具 collect_dynamic_libs(destdir='.') —— 推荐做法
2. 显式枚举 PIL/.dylibs/ 和 PIL/.libs/ —— 兜底
3. 详细打印每一步收集结果 —— 下次出错直接看日志定位
"""
import sys
from pathlib import Path
from PyInstaller.utils.hooks import collect_dynamic_libs
IS_MAC = sys.platform == 'darwin'
IS_WIN = sys.platform == 'win32'
print('=' * 60)
print(f'[spec] 平台: {sys.platform}, Python: {sys.version.split()[0]}')
# ----- 图标 -----
if IS_MAC:
ICON = 'zb100_mac.icns'
elif IS_WIN:
ICON = 'zb100_windows.ico'
else:
ICON = None
# ----- 数据文件 -----
datas = [('config.json', '.')]
if IS_WIN:
datas.append(('zb100_windows.ico', '.'))
# ===== Pillow 原生库收集 =====
# 策略 1: PyInstaller 官方推荐
try:
libs_std = collect_dynamic_libs('PIL', destdir='.')
print(f'[spec] collect_dynamic_libs("PIL", destdir=".") 返回 {len(libs_std)} 项:')
for src, dst in libs_std:
print(f'[spec] STD {Path(src).name} -> {dst}')
except Exception as e:
libs_std = []
print(f'[spec] collect_dynamic_libs 异常: {e}')
# 策略 2: 显式枚举 PIL 包里的原生库目录
libs_extra = []
pil_dir = None
try:
import PIL
pil_dir = Path(PIL.__file__).parent
print(f'[spec] PIL 安装位置: {pil_dir}')
for sub in ('.dylibs', '.libs'):
d = pil_dir / sub
if d.is_dir():
print(f'[spec] 发现 {sub}/ 目录, 内容:')
for lib in sorted(d.iterdir()):
print(f'[spec] {lib.name} ({lib.stat().st_size} bytes)')
if lib.suffix.lower() in ('.dylib', '.so', '.dll'):
libs_extra.append((str(lib), '.'))
else:
print(f'[spec] {sub}/ 不存在')
except Exception as e:
print(f'[spec] 枚举 PIL 失败: {e}')
# 合并去重 (按源路径)
seen = set()
pil_native_libs = []
for src, dst in libs_std + libs_extra:
if src not in seen:
seen.add(src)
pil_native_libs.append((src, dst))
print(f'[spec] 最终 Pillow 原生库数量: {len(pil_native_libs)}')
for src, dst in pil_native_libs:
print(f'[spec] FINAL {Path(src).name} -> {dst}')
if IS_MAC and len(pil_native_libs) == 0:
print('[spec] !!! 警告: macOS 构建但未发现 Pillow 原生库, 打包产物必然启动失败 !!!')
print('=' * 60)
a = Analysis(
['image_generator.py'],
pathex=[],
binaries=pil_native_libs,
datas=datas,
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='ZB100ImageGenerator',
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=[ICON] if ICON else None,
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='ZB100ImageGenerator',
)
if IS_MAC:
app = BUNDLE(
coll,
name='ZB100ImageGenerator.app',
icon=ICON,
bundle_identifier=None,
)